CloudEvents and BabelQueue both put a structured, language-neutral envelope around a message, and that is roughly where the resemblance ends. CloudEvents standardizes how an event describes itself so any producer, protocol and platform can agree on the metadata around an occurrence. BabelQueue freezes what a queue job looks like on the wire — the wire envelope — so six language SDKs execute the same work item off the broker you already run.
That difference in scope decides everything downstream: what each one pins down, what each one deliberately leaves open, and which of them you should actually adopt. This post compares them on the details, and says plainly where CloudEvents is the better answer — because for a large class of systems it is.
What each one standardizes
CloudEvents
CloudEvents is a CNCF specification (graduated in January 2024) for
describing event data in a common way. Version 1.0 defines four required context attributes
— id, source, specversion and type — and four optional ones: datacontenttype,
dataschema, subject and time. An event MAY carry domain data in data, whose media type
is declared by datacontenttype. Producers MUST ensure source + id is unique per distinct
event, and type SHOULD be prefixed with a reverse-DNS name, e.g.
com.github.pull_request.opened.
Around that core, the spec family defines event formats (JSON, Avro, Avro Compact, Protobuf,
XML) and protocol bindings (HTTP, AMQP, Kafka, MQTT, NATS, WebSockets), each with a
structured content mode (metadata and data together in the body, e.g.
application/cloudevents+json), a binary mode (data in the body, attributes mapped to the
protocol’s own metadata — ce--prefixed headers in the HTTP binding), and a batched mode. Any
number of extension attributes may be added; the documented ones include Distributed Tracing,
which carries W3C traceparent and tracestate as event attributes.
The scope line is drawn explicitly. “Inclusion of protocol-level routing information” is a stated non-goal, and the specifications “will not focus on the processing model of either the event producer or event consumer.” There is no attempt counter, no dead-letter concept and no redelivery rule anywhere in the format — that belongs to whatever platform is moving the event.
BabelQueue
BabelQueue defines exactly one thing — the bytes on the queue — and freezes it:
{
"job": "urn:babel:orders:created",
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
"data": { "order_id": 1042 },
"meta": {
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
"queue": "orders",
"lang": "php",
"schema_version": 1,
"created_at": 1749132727000
},
"attempts": 0
}
All five top-level fields are required. Identity is the URN in job, never a class name.
data is a pure JSON object — the cross-language rules for numbers, time and binary are part
of the contract, not implementation notes. meta has five required keys; a producer may add its
own, but consumers must ignore unknown ones and nothing extra may be required for routing.
attempts is the single mutable field, kept deliberately outside the immutable meta block.
There is a published list of forbidden fields, and meta.schema_version is frozen at 1
(the envelope).
On top of that sit the pieces a queue actually needs: a <queue>.dlq dead-letter convention with
an additive dead_letter block, consumer dedupe keyed on meta.id, a transactional outbox, DLQ
redrive — and framework adapters (a drop-in Laravel queue driver, a Symfony Messenger serializer,
Spring AMQP, NestJS/BullMQ, MassTransit, Celery) so your existing worker keeps its retry and
supervision and only the serialization changes.
Where they genuinely overlap
Three overlaps are real, not superficial.
A structured envelope with required metadata. Both refuse to let a message be an opaque blob, and both define where its identity, its message id and its timestamp live instead of leaving that to each producer.
A stable string type identifier. CloudEvents type and BabelQueue job solve the same
problem: name the kind of message with a string the receiving code can match on, rather than a
class or type shared between services. CloudEvents recommends reverse-DNS
(com.github.pull_request.opened); BabelQueue recommends
urn:babel:<context>:<event>. Neither is enforced by the libraries; both
are conventions teams are expected to hold themselves to.
Bindings that project onto native protocol metadata. CloudEvents binary mode maps attributes
onto the protocol’s own metadata — ce--prefixed headers in HTTP — so a router never parses the
body. BabelQueue’s
broker bindings do the analogous thing: the URN goes to AMQP
type and trace_id to correlation_id, or to SQS MessageAttributes, or to an Azure Service
Bus Subject.
There is an important asymmetry inside that last one. In CloudEvents binary mode the headers
are the event metadata and the body holds only data. In BabelQueue the body is always the
complete canonical envelope, and the native metadata is a redundant projection of it — never a
replacement. If a broker, proxy or bridge drops headers, a binary-mode CloudEvent loses its
identity while a BabelQueue message does not.
Correlation is a fourth, weaker overlap. CloudEvents can carry traceparent/tracestate via an
optional extension; BabelQueue requires trace_id on every envelope and forwards it
unchanged across every hop, with W3C traceparent riding out of band on the transport for exact
span linkage (observability).
Side by side
| CloudEvents 1.0 | BabelQueue (schema_version: 1) |
|
|---|---|---|
| Standardizes | how an event describes itself, across protocols | what a queue job looks like on the wire, across languages |
| Envelope | 4 required + 4 optional context attributes, plus extensions | 5 required top-level fields; meta has 5 required keys |
| Identity | type — SHOULD be reverse-DNS |
job — a URN, never a class name |
| Payload | data, any media type via datacontenttype |
data, a pure JSON object, with cross-language encoding rules |
| Serializations | JSON, Avro, Avro Compact, Protobuf, XML | one: compact UTF-8 JSON |
| Where metadata lives | body (structured) or protocol headers (binary) | always the body; native metadata is a redundant projection |
| Version field | specversion (1.0) |
meta.schema_version (1, frozen) |
| Extensibility | any number of extension attributes | additive optional keys; unknown keys ignored; forbidden-field list |
| Transports | HTTP, AMQP, Kafka, MQTT, NATS, WebSockets | Redis, RabbitMQ, SQS, Azure Service Bus, Pulsar, Kafka, Artemis |
| Retry / delivery | not part of the event format | attempts in the envelope; <queue>.dlq + dead_letter block |
| Correlation | optional Distributed Tracing extension | required trace_id on every message |
| Runtime | SDKs in nine languages | 6 SDK cores + framework adapters that reuse your worker |
| Governance | CNCF graduated project, 40+ listed adopters | ADR-governed, cross-SDK conformance suite in every SDK’s CI |
One caveat on that transports row: per-SDK broker coverage is not uniform. PHP has no Azure Service Bus transport, and that is a documented deferral rather than a backlog item — Service Bus speaks AMQP 1.0 and has no modern official PHP client.
Where they genuinely differ
Describing a message versus running one
CloudEvents is deliberately neutral about what a message means: “the purpose, or semantic
meaning, of an event is out of scope,” so a CloudEvent can perfectly well carry a command. What
it does not carry is any of the bookkeeping a worker queue runs on. BabelQueue assumes from the
first line that something will pick the message up, execute it, fail, retry and eventually give
up — which is why attempts is a top-level mutable field, why <queue>.dlq is a convention
rather than an afterthought, and why the dedupe key is fixed at meta.id.
That is a scope difference, not a defect on either side. A format that also has to describe an HTTP webhook cannot sensibly mandate a retry counter; a format for queue jobs cannot sensibly leave one out.
What is frozen versus what is left open
CloudEvents leaves the serialization format, the payload media type and the extension attribute
set open, and that openness is exactly what lets it span HTTP webhooks, Kafka topics and MQTT
devices. The cost is that “we both speak CloudEvents” does not by itself mean “I can read your
messages”: a Protobuf-format event over Kafka and a JSON-format event over HTTP are both perfectly
conformant, so two systems still have to agree on the event format, the content mode and the shape
of data before anything interoperates.
BabelQueue takes the opposite trade. One serialization, one required shape for data, a fixed
required meta key set, a forbidden-field list, and byte-comparable golden fixtures every SDK
runs in CI — so two SDKs that pass conformance can read each other’s messages with no further
agreement. That buys drop-in cross-language consumption and pays for it in flexibility. A wart in
schema_version: 1 is a wart we live with, because
the freeze is the product.
Queue operations are in scope for one of them
Dead-lettering to <queue>.dlq, redrive with dry-run and sandbox routing, dedupe on meta.id,
a transactional outbox: all of that ships with the SDKs and none of it has a CloudEvents
equivalent, by scope rather than by omission. The practical consequence is simply that if you
standardize on CloudEvents for queue work, those parts are yours to build, buy or inherit from
the platform underneath.
What you actually install
CloudEvents SDKs give you the event type and the protocol bindings; the dispatch loop is yours. BabelQueue ships framework adapters, so a Laravel, Symfony, Spring or NestJS worker keeps its retry, backoff and supervision and changes only its serializer. One honest caveat: the PHP reference transports are produce-side, and apart from the framework-less Kafka and Pulsar consumers, consuming in PHP goes through a framework worker.
Where CloudEvents is the better fit
Not hedging — these are cases where adopting BabelQueue would be the wrong call:
- You publish beyond queues. HTTP webhooks, MQTT devices, NATS, WebSockets. CloudEvents has a binding for each; every BabelQueue binding is a message broker, and there is no HTTP binding.
- Your platform already speaks it. Knative, Azure Event Grid, Google Eventarc, Dapr, Tekton, OpenFaaS and others emit and consume CloudEvents natively. Putting a BabelQueue envelope into that world means writing and maintaining the mapping yourself.
- Your payload is not a JSON object. Avro, Protobuf, XML, a large binary blob, or any
arbitrary media type. BabelQueue’s
datais a pure JSON object, full stop — base64 and a documented field is the only escape hatch. - You want a description, not a job. If nothing retries and nothing dead-letters, and the consumers are unknown to the producer, CloudEvents’ smaller required attribute set is simply less to agree on.
- Maturity and reach matter to you. CloudEvents is a CNCF graduated project with SDKs in nine languages and a long adopter list. BabelQueue is far younger and far smaller. That is a real risk factor and worth weighing honestly against the drop-in fit.
Can you use both?
Nothing in the contract stops you from carrying a JSON-format CloudEvent as the data object of
a BabelQueue envelope — data must be a pure JSON object, and a structured-mode CloudEvent is
one. The identity fields map mechanically: type ↔ job.
But no SDK does that mapping for you today, so you would own it, and you would be carrying two identity fields — decide up front which one routes, or you will eventually have messages where they disagree. It is worth doing when a CloudEvents-native system genuinely sits on one side of the queue, and not otherwise.
Choosing
- Cross-language queue jobs over the broker you already run, with a framework worker you want to keep. BabelQueue. That is the case it was designed for.
- Event notifications fanning out across protocols, platforms and unknown consumers. CloudEvents, and it is not close.
- A CloudEvents platform on one side and polyglot workers on the other. Both, with an explicit mapping you own at the boundary.
The two specs answer different questions, so “which is better” is the wrong frame. The useful question is whether your messages need delivery bookkeeping — retries, a dead-letter destination, dedupe — or a faithful description that can travel over anything. If it is the former, read the wire contract for exactly what travels on the queue, or start with what a polyglot queue is and the glossary.