Skip to main content

Postgres Broker for Dramatiq Task Queue

Project description

dramatiq-postgres

Postgres broker for Dramatiq. Your task queue lives in the database you already have — no Redis, no RabbitMQ.

Install

$ pip install dramatiq-postgres psycopg2-binary

Usage

Create the schema (idempotent, safe to run on every deploy):

$ dramatiq-postgres init

Declare the broker and your actors:

# tasks.py
import dramatiq
from dramatiq_postgres import PostgresBroker

dramatiq.set_broker(PostgresBroker(url="postgresql:///mydb"))

@dramatiq.actor
def hello(name):
    print(f"hello {name}")

Send messages from anywhere:

hello.send("world")
hello.send_with_options(args=("later",), delay=60_000)  # in a minute

Run workers:

$ dramatiq tasks

That's it. Results are built in too:

@dramatiq.actor(store_results=True)
def add(a, b):
    return a + b

message = add.send(2, 2)
message.get_result(block=True)  # 4

Django

$ pip install dramatiq-postgres[django]
  1. Add dramatiq_postgres.django to INSTALLED_APPS.
  2. Run manage.py migrate — the queue schema is created by a regular Django migration.
  3. Declare actors in an actors.py module inside your apps.
  4. Start workers:
$ dramatiq dramatiq_postgres.django.worker

The broker connects using your default database automatically. To customize, declare a DRAMATIQ_BROKER setting:

DRAMATIQ_BROKER = {
    "OPTIONS": {},        # PostgresBroker kwargs; url/pool default to DATABASES
    "MIDDLEWARE": [],     # dotted paths or instances of extra middleware
    "ENCODER": None,      # dotted path of a dramatiq encoder class
    "DATABASE_ALIAS": "default",
}

Configuration

All PostgresBroker options:

Option Default Description
url "" libpq URL or kwargs dict; ?maxconn=16 caps the pool
pool None bring your own psycopg2 pool instead of url
results True enable the result backend and middleware
schema dramatiq Postgres schema holding the tables
prefix "" table name prefix
listen True LISTEN for instant delivery; set False behind pgbouncer
notify True NOTIFY on enqueue; set False for maximum enqueue throughput
poll_interval 1.0 seconds between polls (the safety net, or the only source of wake-ups with listen=False)
heartbeat_interval 15.0 seconds between worker heartbeats
heartbeat_ttl 60.0 seconds without heartbeat before a worker is considered dead
maintenance_interval 30.0 seconds between maintenance runs
purge_maxage "30 days" how long rejected messages are kept

The CLI ships maintenance commands, all honoring --dsn, --schemaname and --prefix:

$ dramatiq-postgres init      # create the schema if missing
$ dramatiq-postgres stats     # message counts by state
$ dramatiq-postgres recover   # requeue stuck consumed messages
$ dramatiq-postgres flush     # delete queued/consumed messages
$ dramatiq-postgres purge     # delete old rejected messages

How it works

Everything is plain Postgres — three tables and LISTEN/NOTIFY. No extension, no ORM, no extra service.

Enqueue. send() INSERTs the message as JSONB into the queue table and fires a NOTIFY on dramatiq.<queue>.enqueue with an empty payload. The NOTIFY is just a doorbell: it wakes workers up, it carries no data.

Claim. Each worker polls with one round trip: a batch of due messages is claimed with FOR UPDATE SKIP LOCKED, ordered by available_at then position (FIFO). Workers never race for the same row and never block each other. A partial index covers exactly the state = 'queued' rows, so the claim stays fast no matter how large the table gets.

Delivery. With listen=True (default), one shared LISTEN connection per worker process turns enqueues into instant wake-ups; the poll_interval is only a safety net. With listen=False (needed behind pgbouncer in transaction pooling mode), workers rely on polling alone.

Delayed messages. delay= writes a future available_at. Scheduling lives server-side in the table — nothing is held in worker memory, so restarts never lose scheduled work.

Ack / results. Acknowledging a message DELETEs its row — the hot table only ever contains pending and in-flight work. Actor results go to the separate result table with a TTL.

Failures. A message that exhausts its retries is kept with state = 'rejected' for inspection, and purged after purge_maxage.

Crash recovery. Every worker upserts a heartbeat row each heartbeat_interval. One worker at a time (elected via advisory lock, every maintenance_interval) requeues messages owned by workers whose heartbeat expired, deletes stale worker rows, and purges old rejected messages and expired results. Kill -9 a worker and its messages are back in the queue within heartbeat_ttl seconds — no manual intervention.

Tables

All in the dramatiq schema (configurable via schema/prefix):

queue — pending and in-flight messages:

Column Type Description
message_id uuid PK Dramatiq message id
queue_name text queue the message belongs to
state enum queued, consumed or rejected
message jsonb the message payload, as encoded by Dramatiq
position bigint monotonic enqueue counter, FIFO tie-breaker
available_at timestamptz do not deliver before this moment (delay/eta)
worker_id uuid worker owning the message while consumed
consumed_at timestamptz when the message was claimed
mtime timestamptz last state change

worker — one row per live worker process:

Column Type Description
worker_id uuid PK worker identity, one per process
heartbeat_at timestamptz last heartbeat

result — actor results, decoupled from the queue:

Column Type Description
message_id uuid PK message the result belongs to
result jsonb encoded actor return value
expires_at timestamptz TTL for automatic purge

Connection budget per worker process: the broker pool (up to maxconn, default 16) plus one LISTEN connection.

Support

If you find this project useful, consider buying me a coffee (or a beer):

Buy Me A Coffee

License

Copyright (c) 2026-present Daniel Gatis

Licensed under the MIT License.

Project details


Download files

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

Source Distribution

dramatiq_postgres-1.0.0.tar.gz (89.9 kB view details)

Uploaded Source

Built Distribution

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

dramatiq_postgres-1.0.0-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

File details

Details for the file dramatiq_postgres-1.0.0.tar.gz.

File metadata

  • Download URL: dramatiq_postgres-1.0.0.tar.gz
  • Upload date:
  • Size: 89.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dramatiq_postgres-1.0.0.tar.gz
Algorithm Hash digest
SHA256 cfe33ff9251becd6c86f314d340ac16dc9d39cc2b99eded35c7a21889599c501
MD5 37967a5230852094b4135dfab6977631
BLAKE2b-256 1fb11c196bc3d47982c0b9214164552917dc59f7c40f788d9751499786d566b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for dramatiq_postgres-1.0.0.tar.gz:

Publisher: release.yml on danielgatis/dramatiq-postgres

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

File details

Details for the file dramatiq_postgres-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for dramatiq_postgres-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24d38a1f46823c2e00ca6bcc7b117375dc6371906a0617e6ca43cd31beee3513
MD5 45adb551c1fcdad8161e79aa2dab780f
BLAKE2b-256 4d313fedcb17ed4276225ebb9ee06826b589a89179c8808c21f89ddb371a2553

See more details on using hashes here.

Provenance

The following attestation bundles were made for dramatiq_postgres-1.0.0-py3-none-any.whl:

Publisher: release.yml on danielgatis/dramatiq-postgres

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page