Skip to main content

ArdiQ

PyPI version Python versions CI License: MIT


A fast distributed task queue with a Rust core and a clean Python API, backed by Redis streams.

ArdiQ runs the worker loop and all Redis I/O in Rust (via PyO3 + tokio); you write tasks in plain Python. The two meet at a single async callback, with the GIL held only for the microseconds it takes to start a task and read its result — so a single process handles high concurrency.

Features

  • 🦀 Rust core — the loop and Redis I/O run on tokio, off the GIL
  • Priority queues — higher-priority tasks are consumed first
  • Delayed & scheduled tasks (delay_ms / schedule_ms)
  • Cron & recurring tasks (@app.cron) — 5-field cron (UTC) or every= intervals
  • Automatic retries with quadratic backoff, configurable per task, or on demand (raise Retry)
  • Enqueue by name (app.send("task", ...)) — producers never import the task module
  • Error hooks (@app.on_error) — send every failed attempt to Sentry or your own reporter
  • Crash recovery — in-flight tasks of a dead worker are reclaimed (XAUTOCLAIM)
  • Results with TTL, plus task status (queued / running / complete / not_found)
  • Abort/cancel (job.abort()) — drops queued tasks and cancels running ones over pub/sub
  • Sync & async tasks — blocking sync functions run in a thread pool
  • CLI worker (ardiq run module:app) and burst mode (drain the queue and exit)

Performance

Because the worker loop and every Redis round-trip run in Rust — off the GIL — ArdiQ delivers top-tier throughput at a fraction of the memory of comparable Python task queues.

Benchmarked head-to-head against arq, Taskiq, Streaq, Celery and Dramatiq on the same machine (1,000 tasks, one worker, 10 concurrent):

Queue Throughput Memory
ArdiQ 🦀 top tier ~34 MB 🪶
Taskiq top tier ~95 MB
Streaq fast ~50 MB
arq fast ~30 MB
  • 🏆 Among the fastest async queues on both CPU- and I/O-bound workloads — effectively tied with the leader.
  • 🪶 Lightest in its class — roughly a third of the memory of the next-fastest queue, and the lowest footprint of any queue at its performance level.
  • 📈 Near the theoretical ceiling on I/O work — practically network-bound, with nothing lost to scheduling.
  • 🎯 Rock-steady — negligible variance run to run.

Throughput is shaped by hardware and workload, and the GIL caps in-process CPU work for every Python queue (ArdiQ included). The full, reproducible suite — with the honest caveats — lives in the benchmark repo.

When to use ArdiQ

Reach for ArdiQ when you want:

  • High concurrency on a small footprint — async-native, with the loop and Redis I/O in Rust, so one process does a lot without eating memory.
  • A modern, typed API@app.task, awaitable enqueue, Job handles, results and status built in.
  • Reliability out of the box — priorities, retries with backoff, delayed and scheduled tasks, and crash recovery via Redis consumer groups.
  • Redis you already run — no extra broker to operate.

Consider the alternatives when:

  • You need to saturate many CPU cores in one process — like every single-process Python queue, ArdiQ runs your task body under the GIL, so CPU-bound work is serial per worker (scale out with more workers). For heavy CPU fan-out, a prefork model (Celery, Dramatiq) can be simpler.
  • You need a large, battle-tested ecosystem today — Celery has years of integrations, schedulers, and dashboards. ArdiQ is young and moving fast.
  • You can't run Redis — ArdiQ is Redis-only by design.

ArdiQ sits alongside arq / Taskiq / Streaq as a modern async queue — its edge is the Rust core (memory and per-task overhead) and a batteries-included API.

Installation

$ pip install ardiq

The base install is the library only — a single runtime dependency (msgpack) — enough to define tasks, enqueue them, and run a worker from your own code (await app.run()). For the ardiq worker command, add the CLI extra:

$ pip install 'ardiq[cli]'

You also need a Redis server — the quickest way is Docker:

$ docker run -d --name ardiq-redis -p 6379:6379 redis

or install it from your package manager (or redis.io).

Building from source (if you want to hack on ArdiQ itself): you'll need Rust and uv. Clone the repo and run uv sync.

Quickstart

Define an app and some tasks (example.py):

from ardiq import Ardiq

app = Ardiq(redis_url="redis://localhost:6379", queue_name="example")


@app.task()
async def add(a: int, b: int) -> int:
    return a + b


@app.task(max_retries=3)
def slow_double(x: int) -> int:   # sync task — runs in a thread
    return x * 2

Start a worker:

$ ardiq run example:app

Enqueue tasks from anywhere and read their results:

import asyncio
from example import add


async def main():
    job = await add.enqueue(2, 3)        # returns a Job handle
    print(job.id)
    print(await job.status())            # 'queued' | 'running' | 'complete'
    print(await job.result(timeout=5))   # waits → TaskResult(success=True, value=5, tries=1)


asyncio.run(main())

Or run the whole thing in one process with python example.py, which enqueues a few tasks and processes them in burst mode.

Enqueuing by name

The side that enqueues doesn't have to be the side that runs. app.send puts a task on the queue by name, so a web service can dispatch work without importing the task module — or its dependencies — at all:

from ardiq import Ardiq
from fastapi import FastAPI

api = FastAPI()
queue = Ardiq(redis_url="redis://localhost:6379", queue_name="example")


@api.post("/reports")
async def create_report(user_id: int):
    job = await queue.send("build_report", user_id, format="pdf")
    return {"job_id": job.id}

Nothing is checked locally: the name is resolved by the worker that picks the task up, and one it doesn't know fails there like any other error. For the enqueue options, app.ref hands back the same handle @app.task returns:

await queue.ref("build_report").options(delay_ms=60_000, priority="low").enqueue(7)

A ref can be enqueued but not called — there is no local function behind it.

Retries and error hooks

A task that raises is retried up to max_retries times, waiting tries² seconds between attempts (or the fixed backoff_ms you configure). Raise Retry to make that call from inside the task instead:

from ardiq import Retry


@app.task(max_retries=5)
async def call_api():
    response = await client.get(URL)
    if response.status_code == 429:
        raise Retry("rate limited", delay_ms=30_000)
    return response.json()

Retry still respects max_retries, so it can't loop forever; when the budget runs out the task fails with it as the error.

@app.on_error runs a hook on every failed attempt, before ArdiQ decides between retrying and failing — this is where a reporter like Sentry goes:

import sentry_sdk


@app.on_error
def report(ctx):
    sentry_sdk.capture_exception(ctx.exc)
    log.warning("%s failed on try %s (retrying: %s)", ctx.name, ctx.tries, ctx.will_retry)

The hook takes an ErrorContext(name, task_id, exc, tries, will_retry), may be sync or async, and can be registered more than once — all of them run. One that raises is logged and never changes the task's outcome.

It fires on timeouts, on every retry, and when a worker is handed a task it doesn't know. It does not fire on abort, nor for a Retry you raised yourself — only when that Retry finally gives up. Hooks run on the worker's event loop, so keep them quick.

Shared resources (lifespan)

Tasks often need something expensive that should be built once per worker, not per task — a database pool, an HTTP client. @app.lifespan registers an async generator that sets up before the loop starts and tears down after it stops:

@app.lifespan
async def lifespan():
    pool = await asyncpg.create_pool(DSN)
    yield {"db": pool}          # entries land on app.state
    await pool.close()


@app.task()
async def count_users() -> int:
    return await app.state.db.fetchval("select count(*) from users")

Yield a mapping to populate app.state, or yield nothing and assign app.state.db = ... yourself. Either way app.state is available to async and sync tasks alike.

The hook only runs inside app.run(), so a process that just enqueues never opens the pool. Teardown runs even if the loop fails, and an exception during setup stops the worker before it takes any work.

Aborting tasks

job.abort() cancels a task whether it is waiting in the queue or already running on some worker:

job = await slow_report.options(delay_ms=60_000).enqueue()

if await job.abort():                # False if it already finished
    result = await job.result(timeout=5)
    print(result.aborted)            # True
    print(result.success)            # False

An aborted task ends as an ordinary failed TaskResult with aborted set, so it never retries and result(timeout=) returns as soon as it settles. What happens depends on where the task is when you call it:

Where the task is What abort does
Waiting on a delay or schedule Dropped and finalized immediately.
Queued for pickup The next worker to reach it skips it instead of running it.
Running The worker holding it cancels it, within about a millisecond.

Cancelling a running task needs a long-running worker: the worker subscribes to the queue's abort channel while it runs, which --burst skips. Aborts are still honored under burst, just not mid-flight.

Because cancellation is asyncio cancellation, a sync task can't be interrupted mid-call — the worker stops waiting on it and reports it aborted, but the thread runs to completion. Async tasks are cancelled at their next await, so a task that swallows CancelledError keeps going.

Recurring tasks

Register a task to run on a schedule with @app.cron — either a standard 5-field cron expression (evaluated in UTC) or a fixed every= interval:

@app.cron("0 3 * * *")            # daily at 03:00 UTC
async def nightly_report():
    ...


@app.cron(every=30)               # every 30s — int/float seconds or a timedelta
async def heartbeat():
    ...

Recurring tasks fire while a worker is running, and each occurrence is an ordinary task with its own result, status, retries and timeout. The cron syntax is the common subset — *, lists ,, ranges a-b, and steps */n — at minute resolution; use every= for sub-minute schedules.

Configuration

Ardiq(...) accepts:

Option Default Description
redis_url redis://localhost:6379 Redis connection URL
queue_name "default" Logical queue (key namespace)
priorities ["default"] Priority names, lowest-first
concurrency 16 Max tasks running at once
prefetch concurrency * 2 Max tasks held in memory (drives backpressure)
idle_timeout_ms 60000 When an unrenewed in-flight task may be reclaimed
result_ttl_ms 300000 How long results live (0 drops, negative keeps forever)
burst False Exit once the queue drains
serializer / deserializer msgpack Wire codec; pass pickle.dumps/pickle.loads to send datetimes/objects
cron_poll_s 1.0 How often the worker restages due @app.cron occurrences

@app.task(...) accepts name, max_retries (default 3), backoff_ms, timeout (seconds), and priority. @app.cron(spec, *, every=…, …) takes those same per-task options plus the schedule. Use task.options(delay_ms=…, schedule_ms=…, priority=…, task_id=…).enqueue(...) for one-off overrides.

Logging

ardiq run configures Python's logging for the process (INFO by default, DEBUG with --verbose/-v) and also initializes the Rust core's own logging at the same level. Worker lifecycle (worker starting, worker stopped) logs at INFO; task lifecycle logs at DEBUG (task started, task succeeded) through WARN (task retry scheduled) and ERROR (task failed, task unknown). Task args, kwargs, and results are never logged.

Logging inside a task is just standard logging — it works the same for async tasks and for sync tasks run via asyncio.to_thread:

import logging

logger = logging.getLogger(__name__)


@app.task()
async def send_email(to: str) -> None:
    logger.info("sending email to %s", to)
    ...

If you embed Ardiq outside the ardiq CLI, call logging.basicConfig(...) yourself (see example.py).

Development

$ docker compose up -d      # Redis on localhost:6379
$ uv run pytest             # test suite (needs Redis)
$ uv run ruff check .       # lint
$ uv run ty check ardiq tests   # type-check

After changing the Rust core, rebuild with uv sync --reinstall-package ardiq.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ardiq-0.3.0.tar.gz (38.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

ardiq-0.3.0-cp39-abi3-win_amd64.whl (1.6 MB view details)

Uploaded CPython 3.9+Windows x86-64

ardiq-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

ardiq-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.9 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

ardiq-0.3.0-cp39-abi3-macosx_11_0_arm64.whl (1.7 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

ardiq-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl (1.7 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file ardiq-0.3.0.tar.gz.

File metadata

  • Download URL: ardiq-0.3.0.tar.gz
  • Upload date:
  • Size: 38.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ardiq-0.3.0.tar.gz
Algorithm Hash digest
SHA256 8fd706a18df3113298c1dd54646007644708ae1174e81bbf1b7e739e995b76db
MD5 2a3eb46ad695f83f490aedf810e3cd24
BLAKE2b-256 8834d4f566a3bf4634d72a502a25fbf6cc6a1ccad9866a1dc59ebb98adfd31a5

See more details on using hashes here.

Provenance

The following attestation bundles were made for ardiq-0.3.0.tar.gz:

Publisher: release.yml on 17tayyy/ardiq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ardiq-0.3.0-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: ardiq-0.3.0-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 1.6 MB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ardiq-0.3.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 60113e44d0eb195d0c3c1a4aa64cbda76d5a47e57bea6043a07d1f39fa178cb4
MD5 4a619530019715bb42c84e2d9487deb5
BLAKE2b-256 4bd1c0905da9de51d30e170c642d9c0ddec58a0ecebfd7ea49e8047af0eb92b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for ardiq-0.3.0-cp39-abi3-win_amd64.whl:

Publisher: release.yml on 17tayyy/ardiq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ardiq-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for ardiq-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e4d3d4ede2eecce02b17f8d3b9dc181588c6c39adcbb8aea2cea6fbe5faba542
MD5 dd8c4c3ac70907d8a550783c6466afae
BLAKE2b-256 f585c45501cb5f100c5b9b24d6919b6142a46ad437a862156cdd3a9ab0a5143a

See more details on using hashes here.

Provenance

The following attestation bundles were made for ardiq-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on 17tayyy/ardiq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ardiq-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for ardiq-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b538022cc331427f6551b0452049e98427dca35c4a537fba3770ef284c057eab
MD5 5a6943324d482ace7163f99c606aff4e
BLAKE2b-256 f03e0500d6da44cd1d442c80d4dcac94e77ca95fd38433002dc643ee9ce97488

See more details on using hashes here.

Provenance

The following attestation bundles were made for ardiq-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on 17tayyy/ardiq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ardiq-0.3.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: ardiq-0.3.0-cp39-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 1.7 MB
  • Tags: CPython 3.9+, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ardiq-0.3.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f9cea4f4d752134de9b12b4e81ae144e03214be2dc2ef1a521e3b8da8202f5c3
MD5 a73b95c4bcf796c510ee0fa7945bfd1a
BLAKE2b-256 e6127c9dd315a1b67d7b06a98bb9e39f9304d22fd5548d70f364eb7e6b5c619b

See more details on using hashes here.

Provenance

The following attestation bundles were made for ardiq-0.3.0-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on 17tayyy/ardiq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ardiq-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for ardiq-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0df0b63586243baf0c47a1565aa1a0b392be93c34af61c5a40fb29aed4e830d6
MD5 125fabbcf69e688bf3d7108771c95691
BLAKE2b-256 e8f05b939d231a876d17eb9a4f5217e6f64d81fba1d1e474c7ae0aaa4b667466

See more details on using hashes here.

Provenance

The following attestation bundles were made for ardiq-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on 17tayyy/ardiq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.0

6 files

0.6.0

6 files

0.5.0

6 files

0.4.0

6 files

This release

0.3.0 This release

6 files

0.2.2

6 files

0.2.1

6 files

0.2.0

6 files

0.1.1

6 files

0.1.0

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page