A Postgres-backed job queue for Python. Jobs are rows. Workers dequeue with SELECT … FOR UPDATE SKIP LOCKED. No Redis, no RabbitMQ, no sidecar — just your existing Postgres instance.

Install

pip install pgwerk

Python 3.11+ and Postgres 14+. For cron expression support:

pip install "pgwerk[cron]"

Quickstart

from pgwerk import Werk, AsyncWorker
import asyncio

app = Werk("postgresql://user:pass@localhost/mydb")

async def send_email(to: str) -> None:
    ...

async def main():
    async with app:
        await app.enqueue(send_email, to="user@example.com")
        worker = AsyncWorker(app=app, queues=["default"], concurrency=10)
        await worker.run()

asyncio.run(main())

See the Quickstart guide for the full picture.

What's different

pgwerk dequeues with SELECT … FOR UPDATE SKIP LOCKED — a pattern Postgres has supported since 9.5. That gives you a few properties you don't get from Redis-backed queues:

  • Transactional enqueue — enqueue inside an open transaction; if it rolls back, the job never exists.
  • SQL visibility — jobs are rows in _pgwerk_jobs. Query them with psql, pgAdmin, or anything else.
  • No broker — your existing Postgres instance is the queue. Nothing new to run or monitor.
  • LISTEN/NOTIFY — workers wake up immediately on enqueue instead of waiting for the next poll cycle.

Limitations

Postgres is not a message broker. At high dequeue rates with many concurrent workers, lock contention on the jobs table becomes the bottleneck. If you're processing millions of jobs per second, a dedicated broker will outperform it.

pgwerk requires Python 3.11+ and psycopg3. It does not support other database drivers, earlier Python versions, or cross-language workers. It is also much newer than Celery or RQ — if you need something battle-tested at scale for years, factor that in.


Quickstart · Workers · Enqueueing · Cron