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]
- Add
dramatiq_postgres.djangotoINSTALLED_APPS. - Run
manage.py migrate— the queue schema is created by a regular Django migration. - Declare actors in an
actors.pymodule inside your apps. - 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):
License
Copyright (c) 2026-present Daniel Gatis
Licensed under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfe33ff9251becd6c86f314d340ac16dc9d39cc2b99eded35c7a21889599c501
|
|
| MD5 |
37967a5230852094b4135dfab6977631
|
|
| BLAKE2b-256 |
1fb11c196bc3d47982c0b9214164552917dc59f7c40f788d9751499786d566b0
|
Provenance
The following attestation bundles were made for dramatiq_postgres-1.0.0.tar.gz:
Publisher:
release.yml on danielgatis/dramatiq-postgres
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dramatiq_postgres-1.0.0.tar.gz -
Subject digest:
cfe33ff9251becd6c86f314d340ac16dc9d39cc2b99eded35c7a21889599c501 - Sigstore transparency entry: 2194736971
- Sigstore integration time:
-
Permalink:
danielgatis/dramatiq-postgres@74152a42796c58cd2e954bf785e9538c75efa476 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/danielgatis
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74152a42796c58cd2e954bf785e9538c75efa476 -
Trigger Event:
push
-
Statement type:
File details
Details for the file dramatiq_postgres-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dramatiq_postgres-1.0.0-py3-none-any.whl
- Upload date:
- Size: 21.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24d38a1f46823c2e00ca6bcc7b117375dc6371906a0617e6ca43cd31beee3513
|
|
| MD5 |
45adb551c1fcdad8161e79aa2dab780f
|
|
| BLAKE2b-256 |
4d313fedcb17ed4276225ebb9ee06826b589a89179c8808c21f89ddb371a2553
|
Provenance
The following attestation bundles were made for dramatiq_postgres-1.0.0-py3-none-any.whl:
Publisher:
release.yml on danielgatis/dramatiq-postgres
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dramatiq_postgres-1.0.0-py3-none-any.whl -
Subject digest:
24d38a1f46823c2e00ca6bcc7b117375dc6371906a0617e6ca43cd31beee3513 - Sigstore transparency entry: 2194736973
- Sigstore integration time:
-
Permalink:
danielgatis/dramatiq-postgres@74152a42796c58cd2e954bf785e9538c75efa476 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/danielgatis
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74152a42796c58cd2e954bf785e9538c75efa476 -
Trigger Event:
push
-
Statement type: