Broker bindings

The envelope is broker-agnostic — it is just the message body. A binding says how a given broker carries that body natively, so a consumer can route by URN and continue a trace without decoding the body, and so a message one SDK produces is consumed byte-for-byte by another over the same broker.

Two rules hold for every binding:

  • The body is always the canonical envelope — compact UTF-8 JSON, byte-identical across SDKs. Native metadata is a redundant, routable projection of the body, never a replacement for it.
  • The envelope is frozen at schema_version: 1. A new broker is purely additive — it never changes the wire format.

Redis

Reliable-queue pattern: RPUSH to produce, a blocking move to a :processing list to reserve, then remove on ack. Redis lists carry no per-message metadata, so routing and tracing read the envelope body directly (job/urn, trace_id). At-least-once via the processing list.

RabbitMQ (AMQP 0-9-1)

The body is the envelope; the contract is also projected onto AMQP properties and headers so a consumer can route on the broker’s metadata:

AMQP fieldCarries
typethe URN (job)
correlation_idtrace_id
message_idmeta.id
header x-attemptsattempts
header x-schema-versionmeta.schema_version
header x-source-langmeta.lang

Messages are application/json, persistent delivery, on durable queues; consumed with basic.get + manual ack (at-least-once).

Amazon SQS

The canonical envelope is the MessageBody. On produce, the transport projects the envelope onto native MessageAttributes — a routable view of the body. Ids and strings are DataType String; counters are DataType Number:

MessageAttributeDataTypeValue
bq-jobStringthe URN (job)
bq-trace-idStringtrace_id
bq-message-idStringmeta.id
bq-schema-versionNumbermeta.schema_version (1)
bq-source-langStringmeta.lang
bq-created-atNumbermeta.created_at as epoch milliseconds

Consuming uses SQS’s native delivery semantics:

  • A receive reserves the message for the visibility timeout; a successfully handled message is removed with DeleteMessage. A failing handler simply does not delete it — SQS redelivers after the visibility timeout (at-least-once).
  • attempts is reconciled to max(body.attempts, ApproximateReceiveCount − 1). The first delivery reads 0; a runtime-incremented count is never lowered; an absent or non-numeric ApproximateReceiveCount is ignored. A drop-in driver that instead surfaces the broker’s native count (e.g. Laravel’s SqsJob::attempts()) documents that divergence.

FIFO queues (.fifo) set MessageGroupId (the configured group, else the queue name) and MessageDeduplicationId = meta.id — unless the queue uses content-based deduplication. Delayed delivery uses DelaySeconds, capped at SQS’s 900-second maximum.

This binding is implemented identically across every SDK that ships an SQS transport (Go, Python, Node, Java, PHP, .NET) and is locked by the conformance suite, so the projected attributes and the reconciled attempts are guaranteed to match.

Azure Service Bus

The canonical envelope is the Body. Azure Service Bus has first-class native slots for almost every envelope concept, so the binding maps onto native message fields and needs only two custom application properties:

FieldValue
Subject (a.k.a. Label)the URN (job) — route on Subject without reading the body
CorrelationIdtrace_id
MessageIdmeta.id (enables ASB duplicate detection)
ContentTypeapplication/json
DeliveryCountbroker-maintained, 1-based — the native attempts source
ApplicationProperties["bq-schema-version"]meta.schema_version (1)
ApplicationProperties["bq-source-lang"]meta.lang
ApplicationProperties["bq-created-at"]meta.created_at (epoch ms, convenience mirror)

Application properties are native AMQP-typed values (numbers stay numbers), not the DataType-wrapped strings SQS uses.

Consuming uses ASB’s PeekLock model:

  • A receive reserves the message for the lock duration; a handled message is removed with Complete. A failing handler Abandons it — the broker redelivers and increments DeliveryCount (at-least-once). At MaxDeliveryCount ASB auto-moves it to the native $DeadLetterQueue sub-queue.
  • attempts is reconciled to max(body.attempts, DeliveryCount − 1): DeliveryCount (1-based) is the native floor (first delivery reads 0), and a runtime-incremented body count is never lowered. The rule is identical for the native-consumer SDKs (.NET, Java, Node) and the runtime-transport SDKs (Python, Go).

Delayed delivery is native — set ScheduledEnqueueTime to now + delay. Auth is a connection string or Azure AD (DefaultAzureCredential); transport is AMQP 1.0 over TLS (or WebSockets). An optional SessionId (FIFO) is opt-in and not a contract field.

This binding is implemented identically across every SDK that ships an ASB transport (.NET, Java, Python, Node, Go) and is locked by the conformance suite. PHP is deferred (no modern official client).

Apache Pulsar

The canonical envelope is the message payload. Pulsar message properties are string→string, so the binding projects a redundant, routable view of the body onto bq- properties (every value stringified) and keeps the body authoritative:

PropertyValue
bq-jobthe URN (job) — route on bq-job without reading the body
bq-trace-idtrace_id
bq-message-idmeta.id
bq-schema-versionmeta.schema_version ("1")
bq-source-langmeta.lang
bq-attemptsattempts — the authoritative count, carried in the body
publishTimemirrors meta.created_at (broker-set; body authoritative)

Properties are strings (Pulsar has no typed properties), so numbers are stringified — unlike ASB’s native AMQP-typed values.

Consuming receives one message at a time:

  • A handled message is acknowledged. A failing handler negativeAcknowledges it — the broker redelivers it (at-least-once) and increments RedeliveryCount. With a native DeadLetterPolicy it eventually moves to the cross-language <queue>.dlq topic.
  • attempts is reconciled to max(body.attempts, RedeliveryCount). RedeliveryCount is 0-based (0 on first delivery), so it maps directly with no −1 — and a runtime-incremented body count is never lowered. The rule is identical for the native-consumer SDKs (.NET, Java, Node) and the runtime-transport SDKs (Python, Go).

Delayed delivery is native — deliverAfter (relative) or deliverAt (absolute), also mirrored on a bq-delay property. The default subscription is Shared, named babelqueue; topics default to persistent://public/default/<queue>. Auth is via the service URL (pulsar:// or pulsar+ssl://) plus any client-configured TLS/token.

This binding is implemented identically across every SDK that ships a Pulsar transport (.NET, Java, Python, Node, Go) and is locked by the conformance suite. PHP is deferred (no modern official client).

Other brokers

Apache Kafka and ActiveMQ/Artemis bindings are specified and accepted; their per-SDK transports ship as additive MINOR releases. The envelope stays schema_version: 1 for all of them.