RRaagul S.
OSSRedisAsyncioPython

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

PYTHON REDIS ASYNCIO Open Source · 2026

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 N prefork-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.