BlitzQ
Async-native task queue for Python, built on asyncio and Redis. Two explicit delivery modes: reliable (Streams, at-least-once, crash recovery) and fast (Lists, at-most-once) plus retries, dead letters, priorities, and rate limiting. Benchmarked against Celery.
BlitzQ
Building a Redis-native task queue that doesn't fight the event loop
The problem
Every time I reached for Celery on an async-first project, I hit the same wall: Celery's concurrency model
is built on processes and threads, with async support (eventlet, gevent) patched on top rather than
designed in from the start. If the rest of the stack is async def end to end, FastAPI handlers,
async DB drivers, async HTTP clients, the worker shouldn't be the one piece still thinking in OS threads.
The brief I set for myself: a task queue that treats asyncio as the native execution model, not an afterthought, while keeping the reliability guarantees (retries, crash recovery, dead letters) that make a task queue trustworthy in production.
The approach
One worker, three lanes
Instead of one concurrency primitive for everything, BlitzQ routes work by kind: async def
tasks run as coroutines directly on the worker's event loop (cheap enough to run hundreds at once), sync
functions spill onto a bounded thread pool, and CPU-bound work goes to a process pool via
executor="process". The task author declares the kind; the worker handles placement.
Commit to Redis, skip the abstraction tax
Celery stays broker-agnostic across RabbitMQ, Redis, SQS, and more, which means an abstraction layer between every task and the wire. BlitzQ only talks to Redis, using Streams or Lists, and that constraint is what makes the performance numbers possible: fewer indirection layers, fewer broker-specific edge cases to code around.
Make the delivery tradeoff explicit, not implicit
Rather than one fixed guarantee, BlitzQ exposes two modes per queue: reliable (Streams, at-least-once, survives a worker crash mid-task) and fast (Lists, at-most-once, minimal round-trips). The engineer decides the tradeoff per queue instead of inheriting a framework default that may not fit.
No pickle, ever
Celery's historical default serializer leaned on pickle, a known security liability if task payloads ever
cross a trust boundary. BlitzQ uses msgspec with a restricted, explicit set of serializable
types and never unpickles data from Redis, passing identifiers instead of live objects as a hard rule
rather than a convention to remember.
What shipped
- Reliable and fast delivery modes, selectable per queue
- Retries with exponential backoff and jitter, configurable per-exception routing (
retry_on/dont_retry_on) - Dead-letter queue with inspect/replay via CLI or API
- Delayed and periodic (cron-style) tasks, with atomic claim semantics so a periodic task fires exactly once even with multiple schedulers running
- Three-level task priority, cross-worker Redis-backed rate limiting
- Push-notified
get_result(), callers block on a Redis notification instead of polling --workers Nprefork-style supervisor for scaling worker processes under one command- A reproducible benchmark suite measuring BlitzQ against Celery, published with the results that favor Celery as well as the ones that don't
A lesson from running it in production CI
A reliability test that simulates Redis going briefly unavailable started failing in GitHub Actions, but
passed locally every time. The cause wasn't in BlitzQ. Redis 7+ blocks the DEBUG command by
default unless it detects a genuinely local connection, and Actions' service containers go through
Docker's network proxy even on localhost. The fix was infrastructure, not code: drop the
services: block (which can't pass command args to the container) for a plain
docker run step where I could set --enable-debug-command yes directly. A
reminder that "fails in CI, passes locally" is often a signal to look one layer down before touching the
code at all.
Outcome
Published to PyPI, MIT licensed, with a documented benchmark suite rather than a bare performance claim. The honest framing, where it wins, where Celery still wins, what's deliberately left out (no chains/chords, no dashboard), is the point: it's built for teams already async-first and Redis-committed who want less broker abstraction and a worker model that matches how the rest of their stack already runs.