Installation¶
Requirements¶
- Python 3.9+
- Django 3.2+
- PostgreSQL
- RabbitMQ
Install¶
pip install djoutbox
Or with uv:
uv add djoutbox
Django setup¶
1. Add to INSTALLED_APPS¶
INSTALLED_APPS = [
...,
"djoutbox",
]
2. Configure DJOUTBOX settings¶
DJOUTBOX = {
# Required
"rmq_url": "amqp://guest:guest@localhost/",
# Optional — defaults shown
"exchange_name": "outbox",
"default_retry_delays": ("1s", "10s", "1m", "5m"),
"prefetch_count": 10,
"batch_size": 50,
"notification_timeout": 60.0,
"expiration": None,
"db_alias": "default",
"sent_archive": {
"enabled": False,
"granularity": "1d",
},
"serializers": (), # see "Custom serializers" below
}
The DJOUTBOX dict feeds both Relay and Worker constructors via **settings.DJOUTBOX. Each ignores keys it doesn't use.
3. Run migrations¶
./manage.py migrate
This creates:
djoutbox_pending— table for unsent messagesdjoutbox_sent— range-partitioned archive tabledjoutbox_notify_insert()trigger functiondjoutbox_notify_trigger— firespg_notifyon insert intodjoutbox_pendingwhensend_after <= NOW()- Indexes on
djoutbox_pending(for efficient polling) anddjoutbox_sent(for queries)
Settings reference¶
| Key | Default | Description |
|---|---|---|
rmq_url |
(required) | RabbitMQ connection string (amqp://user:pass@host:port/vhost) |
exchange_name |
"outbox" |
RabbitMQ topic exchange name |
default_retry_delays |
("1s", "10s", "1m", "5m") |
Default retry delays for all consumers |
prefetch_count |
10 |
RabbitMQ prefetch count per consumer |
batch_size |
50 |
Messages to fetch and publish per relay batch |
notification_timeout |
60.0 |
Max seconds to wait for PG NOTIFY before checking for scheduled messages |
expiration |
None |
Default message expiration in RabbitMQ (relay-level) |
db_alias |
"default" |
Which DATABASES entry to use for the relay |
sent_archive.enabled |
False |
Whether to move sent messages to djoutbox_sent (false = delete) |
sent_archive.granularity |
"1d" |
Partition granularity: "Nd" (N days) or "Nm" (N months) |
Custom serializers¶
serializers is a sequence of (type, serializer, deserializer) tuples consulted by both publisher and worker:
from pydantic import BaseModel
DJOUTBOX = {
...,
"serializers": [
(BaseModel,
lambda m: m.model_dump_json().encode(),
lambda cls, d: cls.model_validate_json(d)),
],
}
- Publisher: first tuple whose
typematchesisinstance(body, type)is used; falls back tobytespassthrough, thenjson.dumps. - Worker: first tuple whose
typematchesissubclass(body_type_hint, type)is used; falls back tobytespassthrough, thenjson.loads.
Validation¶
Settings are validated on:
- Django startup —
DjoutboxConfig.ready()callsvalidate_settings(), raisingImproperlyConfiguredon bad values build_dsn()call — the relay entrypoint validates settings even without full Django app loading
Database schema¶
djoutbox_pending¶
| Column | Type | Notes |
|---|---|---|
id |
BIGINT GENERATED BY DEFAULT AS IDENTITY |
Primary key |
routing_key |
TEXT |
RabbitMQ routing key |
body |
BYTEA |
Serialized message body |
tracking_ids |
JSONB |
Chain of UUIDs for tracing |
created_at |
TIMESTAMPTZ |
When the message was created |
send_after |
TIMESTAMPTZ |
Not sent before this time (for eta) |
expiration |
INTERVAL |
Optional TTL in RabbitMQ |
Index: (send_after, created_at) for efficient polling.
djoutbox_sent (partitioned)¶
| Column | Type | Notes |
|---|---|---|
id |
BIGINT |
Copied from pending |
routing_key |
TEXT |
|
body |
BYTEA |
|
tracking_ids |
JSONB |
|
created_at |
TIMESTAMPTZ |
Partition key |
send_after |
TIMESTAMPTZ |
|
expiration |
INTERVAL |
|
sent_at |
TIMESTAMPTZ |
When the relay published it |
Partitioned by RANGE (created_at). Partitions are created automatically by the relay. Indexes on created_at and routing_key propagate to all partitions.