Configure the Python SDK
Last updated:
Create a BabelQueue runtime and point it at your broker with a URL. The
transport is chosen from the URL scheme — no separate “broker” flag.
Broker URL
from babelqueue import BabelQueue
# Redis (extra: babelqueue[redis])
app = BabelQueue("redis://localhost:6379/0", queue="orders")
# RabbitMQ (extra: babelqueue[amqp])
# app = BabelQueue("amqp://guest:guest@localhost:5672/", queue="orders")
# Amazon SQS (extra: babelqueue[sqs])
# app = BabelQueue("sqs://eu-central-1", queue="orders")
# Azure Service Bus (extra: babelqueue[azureservicebus])
# app = BabelQueue("sb://<namespace>.servicebus.windows.net", queue="orders")
# Apache Pulsar (extra: babelqueue[pulsar])
# app = BabelQueue("pulsar://localhost:6650", queue="orders")
# Apache Kafka (extra: babelqueue[kafka])
# app = BabelQueue("kafka://localhost:9092", queue="orders")
# Apache ActiveMQ Artemis (extra: babelqueue[artemis])
# app = BabelQueue("artemis://localhost:5672", queue="orders")
# In-process, great for tests/local — no broker required
# app = BabelQueue("memory://", queue="orders")
| Scheme | Transport | Requires |
|---|---|---|
memory:// |
In-process (tests / local) | built in |
redis:// |
Redis (reliable-queue pattern) | babelqueue[redis] |
amqp:// |
RabbitMQ (via pika) |
babelqueue[amqp] |
sqs:// |
Amazon SQS (via boto3) |
babelqueue[sqs] |
sb:// |
Azure Service Bus (via azure-servicebus) |
babelqueue[azureservicebus] |
pulsar:// |
Apache Pulsar (via pulsar-client) |
babelqueue[pulsar] |
kafka:// |
Apache Kafka (via confluent-kafka) |
babelqueue[kafka] |
artemis:// |
Apache ActiveMQ Artemis (via python-qpid-proton) |
babelqueue[artemis] |
The sqs:// URL is sqs://<region> with optional query params: endpoint
(LocalStack/ElasticMQ), prefix (a queue-URL prefix that skips GetQueueUrl), fifo=1,
group_id, wait_time. The transport sends the canonical envelope with the
§3 MessageAttributes and reconciles
attempts to max(body.attempts, ApproximateReceiveCount − 1). With dead_letter=True on SQS the
dead-letter queue (since 1.24.0) defaults to <queue>-dlq (FIFO: <queue>-dlq.fifo), because SQS does not accept a dot in a queue
name; dead_letter_queue or dead_letter_suffix override it (on a FIFO queue a suffix goes before .fifo:
orders.fifo + -dlq is orders-dlq.fifo).
The sb:// URL is sb://<namespace>.servicebus.windows.net and authenticates with
DefaultAzureCredential (Azure AD); pass connection_string=... to AsbTransport for
SAS auth, or BabelQueue(transport=...) with a built client. It maps the envelope onto
native Service Bus fields (§4) and
reconciles attempts to max(body, DeliveryCount − 1).
The pulsar:// URL is pulsar://<host>:<port> (or pulsar+ssl://); pass a built client
via BabelQueue(transport=...) for TLS/token auth. It projects the envelope onto native
Pulsar message properties (bq-job = URN, plus the bq- fields,
§5), defaults to a Shared subscription
named babelqueue, and reconciles attempts to max(body, redelivery_count) — the
redelivery count is 0-based, so no −1. A retry re-publishes the message to the same topic with
bq-attempts + 1 and acknowledges the original, so max_attempts (default 3) bounds it on every
subscription type; when the attempts are used up the runtime writes <queue>.dlq with
dead_letter.attempts set to the counted value (with the default, 3), or drops the message and
logs it when dead-lettering is off. A failed DLQ write or retry publish negatively acknowledges the
original, so the broker redelivers it.
The kafka:// URL is kafka://<host>:<port>[,<host>:<port>]; pass an injected producer +
consumer_factory for custom auth. The record value is the envelope, projected onto bq-
headers (§6); consume is process-then-commit
(enable.auto.commit=false) and attempts reconciles to the authoritative bq-attempts
header (else the body). Kafka has no native delay/DLQ, so the runtime owns retry by
republishing with attempts+1 and dead-letters to <queue>.dlq. The transport keeps the
record it last handed out until it is acknowledged or rewound.
The runtime’s own settle writes (the retry republish, the release, and the envelope, poison
and schema-version-rejection dead-letter writes) go through publish_settle(), which waits for
the broker to confirm that write: it uses the write’s own delivery report and waits up to
delivery_timeout seconds (the optional KafkaTransport(delivery_timeout=30.0) argument). On
a delivery error or a timeout it raises KafkaDeliveryError, nothing is committed, and the next
poll seeks the partition back to the record, so it is redelivered instead of being skipped by
the next record’s commit (the handler may run again; keep handlers idempotent). A failed seek
is logged and does not block a rebalance. A missing <queue>.dlq topic or an unavailable
partition leader therefore no longer loses the record. Your own app.publish() calls, also
from inside a handler, are unchanged: fire-and-forget, with no wait for the broker.
A poison record (a value that is not valid UTF-8, not valid JSON, not a JSON object, or that has a
member of the wrong type) never raises in pop(). With a DLQ on it is never retried; it is
dead-lettered like the Go reference (ADR-0035). A body that is a strict RFC 8259 JSON object (valid
UTF-8, no top-level dead_letter member, no NaN / Infinity literal, no unpaired surrogate escape,
nested within the interpreter’s recursion limit) is spliced byte for byte: every original character is
kept, numbers such as 1e400 or an integer beyond 2^53 stay as written, and only a dead_letter
member is appended. Anything else becomes {"raw": "<body>"} (bytes that are not UTF-8 are carried
as standard base64 with "raw_encoding": "base64", never with U+FFFD substitutions). Either way the
dead_letter block carries reason: "poison", original_queue, attempts (from the bq-attempts
header) and lang. The record key is preserved, and the offset is committed only once
the DLQ write is confirmed (a failed write leaves it uncommitted and rewinds). Poison follows the rules the Go reference applies, with two intentional
differences: a value that is not valid UTF-8 is poison here (Go substitutes U+FFFD and accepts it),
and JSON nested deeper than the interpreter’s recursion limit (about 1000 levels, where Go allows
10000) is poison. Member names
are matched as spelled (a JOB or Meta member is an unknown member, not a typed one), and
duplicate keys are each checked. Whatever a decode raises after the shape scan is poison too, so a
decode error never stops the consumer.
How a poison record is settled depends on on_unknown_urn and on whether a DLQ is configured, like
the Go reference’s routePoison:
UnknownUrnStrategy.DELETEdrops it (and commits), even when a DLQ is configured.- Otherwise, with a DLQ on (
dead_letterenabled, orUnknownUrnStrategy.DEAD_LETTER), it is written to the DLQ without being retried. - Otherwise (no DLQ) it takes the bounded retry path through the retry tiers and is dropped once
max_attemptsis reached. Withoutretry_tiers_msthere is no tier to retry on, so the record is committed, dropped and logged and the partition advances (it used to be left uncommitted and block the partition). The runtime’s start-up warning for that configuration says the same. Enabledead_letterto keep such records in<topic>.dlq, or configureretry_tiers_msfor the bounded retry. This applies to poison only: a failing handler, orreleaseof a valid envelope, still raisesBabelQueueExceptionwithout tiers.
An identity-less JSON object (one with neither a valid string job nor a valid string urn) is
poison too, not an unknown URN: it is never handed to a handler and never takes the
on_unknown_urn strategies, which are for a valid envelope whose URN has no handler. On Redis it
is dead-lettered unconditionally, like every Redis poison body, in both key layouts.
On the other transports (sqs://, amqp://, pulsar://, sb://, artemis:// and memory://)
a body that does not decode is no longer decoded into an empty envelope and retried as an unknown
URN. It takes the same poison route as above: with a DLQ on it is written to <queue>.dlq at once,
without retry, as the poison record with dead_letter.reason: "poison" and the original body kept
(it used to be reason: "unknown_urn" with no original body); UnknownUrnStrategy.DELETE drops it;
with the DLQ off RELEASE is replaced by the bounded FAIL path (re-published with attempts + 1
up to max_attempts, then dropped and logged), except on sqs:// and sb://, where a poison body
with the DLQ off is not retried and not dropped but handed to the broker’s own mechanism on its first
delivery (SQS: left undeleted, so visibility expiry redelivers it and the queue’s RedrivePolicy takes
it; Azure Service Bus: abandoned until MaxDeliveryCount, or dead-lettered natively;
ADR-0037, since 1.24.0). The count the bound uses is the larger of the
transport’s native delivery count (Pulsar, SQS, Azure Service Bus and Artemis report one) and the
attempts of the re-published body, so a poison body is bounded on every transport.
The bq-schema-version gate
rejection is never retried: with a DLQ on it dead-letters the incoming body raw and unchanged
(no dead_letter block), with no DLQ it is dropped with a logged warning, and with
UnknownUrnStrategy.DELETE it is acknowledged and dropped before the gate runs. A bq-attempts header that is not valid UTF-8 does not
raise either. Consumers of the DLQ that expected the raw bytes of a malformed record must read the
wrapped body.
The retry republish, the envelope dead-letter write, the raw dead-letter write, the redrive
write back to the work topic and a redrive restore all keep the consumed record’s key (ReceivedMessage.key, None for a null key): a
null key stays null, an empty key stays empty, and a key is never invented. Partition equality
across SDKs is not guaranteed.
The artemis:// URL is artemis://<host>:<port> (or artemis+ssl://) — Apache ActiveMQ
Artemis over AMQP 1.0 (python-qpid-proton, not RabbitMQ’s 0-9-1 pika); pass a built
connection via BabelQueue(transport=...) for auth/TLS. It projects the envelope onto the AMQP
a JMS peer reads — x-opt-jms-type = URN, correlation-id = trace_id, plus the string bq_
application properties (underscored — JMS property names must be valid Java identifiers; §7) — and
reconciles attempts to max(body, delivery_count); the AMQP delivery-count is 0-based, so
no −1.
Schema-version gate
The sqs://, sb://, kafka:// and artemis:// transports check the bq-schema-version
message attribute / property / header (Artemis: bq_schema_version) before the body is
decoded. A missing or blank value (the empty string, or only ASCII whitespace: space, tab, LF,
VT, FF, CR) changes nothing: the body is decoded as usual. A header or property present with
a null value counts as missing too. Any other value is compared with "1" exactly as sent,
with no trimming, so "2", "x", "01" and " 1" are all rejected without decoding the
body. On Azure Service Bus an integer 1 is accepted as well as the string "1"; a float
(including 1.0) or a boolean is rejected. A "1" value does not skip the body check: the
decoded envelope still goes through the schema_version validation.
A rejected delivery reaches the app on a reserved route,
babelqueue:unsupported-schema-version:<version>. The transport gates without decoding the
body, and the app does not decode it either. No handler can match the route and a retry cannot
change the outcome, so it is never retried.
on_unknown_urn="delete" wins over dead-lettering: the delivery is acknowledged and dropped.
Otherwise, with dead_letter=True (or on_unknown_urn="dead_letter") the incoming body goes
straight to the dead-letter queue byte-identical, with no dead_letter block and no
unknown_urn reason. With dead-lettering off it is acknowledged and dropped on kafka:// and
artemis://, while sqs:// and sb:// hand it to the broker’s own mechanism instead (SQS: left
undeleted for the RedrivePolicy; Azure Service Bus: abandoned or dead-lettered natively), so nothing
is dropped there (ADR-0037, since 1.24.0).
Options
| Option | Default | Description |
|---|---|---|
queue |
default |
Queue name written to meta.queue |
max_attempts |
3 |
Retries before a message is exhausted (bumps attempts) |
dead_letter |
False |
Quarantine exhausted messages on <queue>.dlq |
on_unknown_urn |
fail |
fail | delete | release | dead_letter |
dead_letter_queue |
None |
Explicit dead-letter queue name (overrides the suffix rule) |
dead_letter_suffix |
None (.dlq; -dlq on sqs://) |
Suffix appended to the source queue to derive the dead-letter queue. Unset, the transport’s default applies: .dlq, or -dlq on SQS (queue names there cannot contain a dot). An explicit value always wins |
retry_backoff |
0.0 |
Seconds a failed message stays invisible before redelivery, on transports that release natively (e.g. SQS); others re-publish immediately |
unknown_urn_release_delay |
0.0 |
Redelivery delay in seconds for the release unknown-URN strategy |
shutdown_timeout |
30.0 |
Seconds a graceful shutdown waits for the in-flight handler before releasing its message |
transport |
None |
A ready-built Transport; when given it is used instead of the one derived from the broker URL |
app = BabelQueue(
"redis://localhost:6379/0",
queue="orders",
max_attempts=3,
dead_letter=True,
on_unknown_urn="dead_letter",
)
Need just the wire codec, with no broker wiring? Import
EnvelopeCodecdirectly — see Producing messages.
Next: Producing messages.