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).
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.
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() and is never retried. It is dead-lettered
wrapped, like the Go reference: a body that is exactly one JSON object keeps its keys, anything
else becomes {"raw": "<body>"} (bytes that are not UTF-8 are stored with U+FFFD substitutions), and
a dead_letter block with reason: "poison", original_queue, attempts (from the bq-attempts
header) and lang is attached. 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). A body whose numbers
cannot be written back as valid JSON (for example 1e400, or an integer beyond 2^53) is carried
verbatim under raw. 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 no
record can wedge a partition.
A poison record goes to the DLQ whatever on_unknown_urn is: UnknownUrnStrategy.DELETE does not
drop it. It is dropped (and committed) only when no DLQ is configured. The bq-schema-version gate
rejection still dead-letters its record raw. 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.
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 |
.dlq |
Suffix appended to the source queue to derive the dead-letter queue |
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.