TaskQ¶
Async-native, Postgres-backed background job library for Python 3.12+¶
TaskQ lets application code enqueue work as strongly-typed jobs that are persisted in
Postgres and executed by one or more worker processes. Because Postgres is the durable store,
you get exactly the transactional guarantees you already rely on — no separate broker, no
message loss across restarts, no split-brain between your application database and your job
state. A single TASKQ_PG_DSN is sufficient to run the full stack.
Why TaskQ?¶
If you are evaluating Python task queues, here is how TaskQ compares to the alternatives:
| TaskQ | Celery | Dramatiq | arq | RQ | |
|---|---|---|---|---|---|
| Broker | Postgres (no external broker) | Redis/RabbitMQ | Redis/RabbitMQ | Redis | Redis |
| Async-native | Yes (asyncio + asyncpg) | No (thread-based) | No (thread-based) | Yes | No |
| Type-safe end-to-end | Yes (Pydantic + pyright strict) | No | No | Partial | No |
| Admin UI | Built-in (FastAPI + htmx) | Via Flower | Via Flower | No | No |
| DI engine | Yes (scoped providers) | No | No | No | No |
| Cron scheduling | Built-in (leader-elected) | celery-beat | periodic | via arq-cron | via rq-scheduler |
| Rate limiting | Built-in (token bucket, sliding window, reservations) | No | No | No | No |
| Observability | OpenTelemetry-native, vendor-neutral | Via extensions | Via extensions | Limited | Limited |
| Batch enqueue | Yes (COPY FROM up to 50K rows) | group() |
No | No | No |
| Cooperative cancellation | Three-phase protocol | No | No | No | No |
When to choose TaskQ:
- You already use Postgres and want durable background jobs without standing up Redis/RabbitMQ
- Your codebase is async-first and you need a worker that speaks asyncio natively
- You want end-to-end type safety from
@actorthroughJobHandle[R].wait() - You need built-in rate limiting, cron scheduling, or dependency injection
- You want a production-grade admin UI out of the box
When to look elsewhere:
- You need a polyglot broker shared across multiple languages (Celery + RabbitMQ)
- You need massive throughput (>50K jobs/sec) where Redis's in-memory dispatch wins over Postgres
- You're on an older Python (<3.12) or don't use async
Features¶
-
Actors
Define typed job handlers with
@actor. Payload and result types are inferred from annotations and validated at decoration time. Bothasync defand sync functions are supported. -
Postgres-Native
The entire job lifecycle — enqueue, dispatch, heartbeat, retry, cancellation — is expressed in SQL. Advisory locks and
SKIP LOCKEDreplace broker semantics. No separate infrastructure required. -
Async-First
Built on
asyncioandasyncpg. Actors areasync deffunctions. The worker and client are both fully async.LISTEN/NOTIFYprovides near-zero-latency dispatch wakeups. -
Type-Safe End-to-End
@actorinfersPandRfrom the handler's annotations.ActorRef[P, R]flows intoJobsClient.enqueue, which returnsJobHandle[R].handle.wait()returnsR. The entire chain is checked by pyright in strict mode. -
Rate Limiting
Token bucket, sliding window, and concurrency reservation primitives backed by Redis or Postgres. Compose multiple limits per actor. Automatic Redis-to-Postgres fallback.
-
Dependency Injection
FastAPI-style signature convention: declare what you need as typed keyword parameters. The worker's DI engine resolves them at dispatch time. Three scope lifetimes (PROCESS, LOOP, TRANSIENT) with cycle detection at startup.
-
Admin UI
Read-only by default observability dashboard built with FastAPI and Jinja2. Live queue, job, worker, schedule, and rate-limit views. Real-time SSE updates when Redis is configured; polling fallback otherwise. Write operations (cancel, retry, run-schedule) are gated behind
TASKQ_ADMIN_ACTIONS_ENABLED(defaultfalse).Fail-closed by default
The admin UI raises
RuntimeErrorat startup in non-dev environments if noauth_dependencyis configured. Configure SSO viataskq[oidc]ortaskq[saml], pass a customauth_dependency, or explicitly opt out withTASKQ_ADMIN_UI_REQUIRE_AUTH=false. See Admin UI — Security. -
Observability
OpenTelemetry-native, vendor-neutral. Spans, metrics, and structured logs are emitted via OTLP. Point
OTEL_EXPORTER_OTLP_ENDPOINTat any OTel-compatible collector. -
Cron Scheduling
Declare periodic schedules with
cron(...). Standard 5-field cron expressions (plus an optional 6th seconds field), timezone support with DST gap/overlap handling, payload factories, and auto-discovery at worker startup. -
Batch Enqueue
enqueue_batch()for transactional fan-out with idempotency keys.enqueue_batch_fast()uses PGCOPY FROMfor up to 50K rows at maximum throughput. -
Cancellation
Three-phase protocol: cooperative (
cancel_event) then forced (task.cancel()) then abandoned. Pending and scheduled jobs are cancelled immediately without worker involvement. -
Progress Tracking
Actors emit structured progress updates via
ctx.progress(). Real-time Redis pub/sub delivery to Python async iterators or HTTP SSE endpoints. Postgres retains the latest snapshot. -
Workgroups
Lightweight process orchestrator for multi-queue deployments. Per-worker configuration from a TOML file with crash restart, health checking, and graceful shutdown propagation.
Installation¶
Or with uv:
Python 3.12+
TaskQ requires Python 3.12 or newer. Core dependencies include asyncpg, pydantic,
opentelemetry-api, and structlog.
Optional extras:
| Extra | Adds | When to use |
|---|---|---|
taskq-py[redis] |
redis>=7.4 |
Real-time progress fanout via Redis pub/sub, Redis-backed rate limiters |
taskq-py[otel] |
opentelemetry-sdk, opentelemetry-exporter-otlp |
Configuring OTel providers programmatically; in-process test utilities |
taskq-py[fastapi] |
fastapi, jinja2, sse-starlette, uvicorn |
Admin UI (taskq ui serve), SSE progress bridge, Prometheus metrics router |
taskq-py[prometheus] |
opentelemetry-exporter-prometheus |
Prometheus metric scrapes |
taskq-py[reload] |
watchfiles |
Autoreload of workers and the admin UI during local development |
Quick Start¶
from pydantic import BaseModel
from taskq import actor, TaskQ
class SendEmailPayload(BaseModel):
to: str
subject: str
body: str
class SendEmailResult(BaseModel):
message_id: str
# Define an actor — payload and result types are inferred from annotations.
@actor
async def send_email(payload: SendEmailPayload) -> SendEmailResult:
print(f"Sending '{payload.subject}' to {payload.to}")
return SendEmailResult(message_id="msg-123")
# Enqueue a job and wait for the result.
async def main() -> None:
from taskq.settings import TaskQSettings
settings = TaskQSettings.load()
async with TaskQ(dsn=str(settings.pg_dsn)) as tq:
handle = await tq.enqueue(
send_email,
SendEmailPayload(to="user@example.com", subject="Hello", body="World"),
)
result = await handle.wait(timeout=30.0)
print(f"sent: {result.message_id}")
# Run the worker:
# taskq migrate up
# taskq worker --actors myapp.actors:registry
Next Steps¶
- Installation — Set up TaskQ in your project
- Quick Start — Go from zero to running worker in minutes
- Actors —
@actordecorator, retry policies, concurrency caps, DI - Jobs & Clients —
JobsClient.enqueue,JobHandle.wait, batch, cancellation - Workers — Worker configuration, pools, heartbeat, graceful shutdown
- Rate Limiting — Token bucket, sliding window, concurrency reservations
- Cron Scheduling — Periodic schedules with
cron() - Dependency Injection — Provider registry, scopes, lifecycle
- API Reference — Full autogenerated API docs
- :material-account-tree: Architecture — Dispatch CTE, advisory locks, leader election internals