Messaging Protocols
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
"Messaging protocol" is used loosely in most conversations to mean anything from a byte-level framing on the wire to a Java interface an application programs against. Those are not the same kind of thing, and confusing them causes real mistakes — most commonly assuming that because two brokers both "speak AMQP", or because two applications both "use JMS", they can talk to each other without a bridge. This page draws that line precisely: it walks the actual wire protocols a client and a broker exchange bytes over — AMQP 0-9-1, AMQP 1.0 (a different, later, non-interoperable protocol despite the shared name), the Kafka binary protocol, and a set of lighter, more specialized protocols (MQTT, STOMP, the RabbitMQ Stream protocol, and WebSocket transports for several of the above) — and then contrasts all of them with JMS/Jakarta Messaging, which is an API specification a provider implements on top of whatever wire protocol it actually speaks, not a wire protocol itself. Apache Kafka and RabbitMQ anchor the comparison throughout, since between them they cover almost every protocol discussed here either natively or via a broker plugin.
AMQP 0-9-1: RabbitMQ’s original native protocol
AMQP 0-9-1 (Advanced Message Queuing Protocol, version 0-9-1) is the protocol RabbitMQ was built around from the start, and it predates the unrelated OASIS AMQP 1.0 standard covered in the next section by several years. It was never formally standardized by a body such as OASIS or ISO — the specification is a document RabbitMQ and the other vendors behind it published and continue to host — but it is implemented compatibly by several other brokers and client libraries beyond RabbitMQ itself.
An AMQP 0-9-1 connection is a single, typically long-lived TCP connection, authenticated once. Opening a new TCP connection per logical unit of work would be wasteful, so the protocol multiplexes many independent channels over that one connection — a channel is, in the specification’s own words, a "lightweight connection that shares a single TCP connection" with every other channel on it. Every protocol operation a client performs happens on some channel, identified by a numeric channel ID carried in every frame, and traffic on one channel is entirely independent of traffic on any other — a slow or blocked consumer on channel 3 does not stall a publisher on channel 7 of the same connection.
The operations themselves are grouped into classes, each a named collection of related methods — the
protocol’s own term for what amounts to an RPC-style command, unrelated to a method in an object-oriented
programming language. The exchange class carries exchange.declare; the queue class carries
queue.declare and queue.bind; the basic class carries basic.publish, basic.consume, and
basic.ack. A client library such as RabbitMQ’s own Java client hides the class/method framing behind ordinary
method calls on Channel, but the underlying frames on the wire are exactly this class/method structure,
addressed to a specific channel ID.
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("rabbitmq.example.com");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("orders-service");
factory.setPassword("changeit");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare("orders-exchange", BuiltinExchangeType.TOPIC, true);
channel.queueDeclare("orders-queue", true, false, false, null);
channel.queueBind("orders-queue", "orders-exchange", "orders.created");
channel.basicPublish("orders-exchange", "orders.created", null,
"order created".getBytes(StandardCharsets.UTF_8));
}
connection.createChannel() opens a new multiplexed channel over the one TCP connection factory.newConnection()
established; exchangeDeclare, queueDeclare, queueBind and basicPublish are the client library’s own names
for the exchange.declare, queue.declare, queue.bind and basic.publish methods on the wire. See the
RabbitMQ Java Client API Guide for the full API surface,
and AMQP 0-9-1 Model Explained for the protocol model this
client is a thin layer over.
AMQP 1.0: a different OASIS standard, not a newer version of the above
Despite sharing the "AMQP" name, AMQP 1.0 is not "AMQP 0-9-1 plus some new features" — it is a different protocol with a different wire format and a different conceptual model, standardized separately by OASIS (and subsequently ISO/IEC 19464) rather than authored by RabbitMQ. A client that only speaks AMQP 0-9-1 cannot talk to an AMQP 1.0 endpoint, and vice versa; a broker that supports "AMQP" in some generic sense needs to say explicitly which of the two it means, and several, including RabbitMQ, support both as two entirely separate protocol implementations rather than one protocol with a version flag.
AMQP 1.0 replaces channels, exchanges and queues with a different vocabulary:
| Term | Meaning |
|---|---|
Container |
A peer capable of sending and receiving messages — a client process or a broker. The protocol’s own term for "the thing at each end of a connection". |
Connection |
A single TCP (or TLS-wrapped TCP) connection between two containers, established once and negotiated for protocol version and capabilities. |
Session |
A bidirectional, sequenced context multiplexed over a connection — the AMQP 1.0 analogue of an AMQP 0-9-1 channel, though the two are not interchangeable: a session additionally tracks transfer IDs and windowed flow control at the session level, on top of the per-link flow control below. |
Link |
A unidirectional route for messages between a source and a target, established within a session — a sending link or a receiving link. Unlike an AMQP 0-9-1 channel, a link has no inherent notion of an exchange or a queue; addressing is just a source/target string the two containers agree on, and any exchange/queue-like routing model is something the broker chooses to implement on top, not something the protocol defines. |
Flow control works differently, too. AMQP 0-9-1 mostly relies on TCP backpressure plus an optional
basic.qos "prefetch count" a consumer sets to cap how many unacknowledged messages it will be sent at once.
AMQP 1.0 makes flow control an explicit, first-class part of the protocol: a receiver grants the sender
credit — a count of how many additional messages it is currently willing to accept — via a flow
performative, and the sender may not transfer more messages on that link than the outstanding credit allows.
Credit is replenished as the receiver processes messages, so throughput is governed continuously by the
receiver’s actual capacity rather than by a single static prefetch number negotiated once.
Delivery outcomes are likewise explicit and named, rather than a single ack/nack:
| Outcome | Meaning |
|---|---|
Accepted |
The receiver has taken responsibility for the message; it should not be redelivered. |
Rejected |
The receiver considers the message invalid or unprocessable outright; typically routed to a dead-letter destination rather than redelivered as-is. |
Released |
The receiver hands the message back without any negative judgment on its content — for example a consumer shutting down mid-processing — and it becomes eligible for redelivery, usually to another consumer. |
Modified |
Like Released, but the receiver may also adjust delivery metadata (for example incrementing a delivery-count annotation) before the message becomes eligible for redelivery — the mechanism most dead-letter and retry logic built on AMQP 1.0 is layered on top of. |
RabbitMQ added native AMQP 1.0 support as a second, independent protocol implementation alongside its original AMQP 0-9-1 stack, and made it a core, first-class part of the broker (rather than an optional plugin, as it had been in earlier releases). It is far from the only implementation: Azure Service Bus and Apache ActiveMQ Artemis, among others, also speak native AMQP 1.0, which is a large part of the protocol’s appeal — unlike AMQP 0-9-1, which is RabbitMQ’s own design, an AMQP 1.0 client can in principle be pointed at any conformant broker. For Azure Service Bus, Event Grid and Event Hubs in depth, see Messaging and Integration Services. For the AWS counterparts (SQS, SNS, EventBridge, Amazon MQ, MSK), see Messaging and Integration.
The Kafka binary protocol
Apache Kafka does not implement AMQP in either form; it defines and implements one protocol of its own, a binary, request/response protocol over TCP that every official Kafka client — producer, consumer, admin, and the brokers talking to each other for replication — speaks. There is no broker-side equivalent of RabbitMQ’s plugin-based protocol support: the Kafka binary protocol is the only wire protocol a Kafka broker accepts.
Every request identifies which operation it is invoking with a numeric API key — Produce, Fetch,
Metadata, OffsetCommit, and so on — and every API key has its own independently evolving request and
response schema, versioned separately from every other API key. A request frame carries both the API key and
an API version number; a broker that does not support the version a client sent simply rejects the request,
and a client and broker negotiate the highest version both sides understand rather than the whole protocol
being versioned as one indivisible unit. This is what lets a Kafka cluster upgrade brokers and clients
independently, in either order, without a lock-step protocol bump across the entire deployment.
A client never needs the address of every broker in a cluster up front. It is configured with a small list of
bootstrap servers — one is technically enough, though several are configured for resilience — connects to
any one of them, and sends a Metadata request, whose response describes every broker currently in the
cluster and which broker leads which topic partition. From that point on, the client caches this metadata and
sends produce/fetch requests directly to the partition leader responsible for the partition in question, only
returning to a Metadata request when it receives an error indicating its cached view is stale (a leader
election moved a partition, for example).
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker-1:9092,broker-2:9092,broker-3:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
producer.send(new ProducerRecord<>("orders", "order-1", "order created"));
}
bootstrap.servers only needs to name enough brokers that at least one is reachable at startup — the producer
above discovers the rest of the cluster, and the current leader for the orders topic’s partitions, entirely
from the Metadata response its client sends once connected. See
Apache Kafka documentation — Producer Configs for the
full configuration reference, and the
Guide to the Kafka Protocol for
the wire-level request/response format. For the Spring/Quarkus programming-model surface built on top of this
client, see Messaging with Apache Kafka (Spring) or
Messaging with SmallRye Reactive Messaging (Quarkus).
Lightweight and specialized protocols
A handful of narrower-purpose protocols sit alongside AMQP and Kafka’s own protocol, each solving a problem neither of those two addresses well.
MQTT
MQTT is a lightweight, binary publish/subscribe protocol originally designed for constrained devices and unreliable networks — sensors, embedded devices, mobile clients on a flaky connection — where a full AMQP or Kafka client would be too heavy. Its message model is deliberately minimal: clients publish to and subscribe on hierarchical topics, with three quality-of-service levels (at most once, at least once, exactly once) and a small, fixed set of control packet types. RabbitMQ supports MQTT (versions 3.1, 3.1.1 and 5.0) through a plugin that maps MQTT topics onto RabbitMQ’s own exchange/queue model underneath; it is not a protocol RabbitMQ implements as part of its core, and it is not something Kafka speaks at all.
STOMP
STOMP (Simple/Streaming Text Oriented Messaging Protocol) is a deliberately simple, text-based, broker-agnostic
protocol — its frames are readable ASCII, closer in spirit to HTTP than to a compact binary encoding, and a
STOMP session can be driven by hand over a raw TCP connection (telnet, for example) for debugging. Like MQTT,
RabbitMQ supports it via a plugin rather than as part of the core broker, translating STOMP’s small frame set
onto the same underlying exchange/queue model.
The RabbitMQ Stream protocol
RabbitMQ’s Streams feature — an append-only, replicated log structure conceptually closer to a Kafka topic than to a classic AMQP queue — has its own dedicated binary protocol, distinct from both AMQP 0-9-1 and AMQP 1.0, listening by default on port 5552 (5551 for TLS). A stream can still be published to and consumed from using an ordinary AMQP 0-9-1 client, but internally a stream stores its messages in AMQP 1.0-encoded form, and publishing through AMQP 0-9-1 pays a conversion cost the dedicated Stream protocol client avoids; the Stream protocol is the recommended way to get the feature’s full throughput and its stream-specific capabilities (offset tracking, super streams), rather than a convenience layered on top of AMQP for compatibility’s sake.
WebSocket transports
Several of the protocols above can also run over a WebSocket connection instead of a raw TCP socket — RabbitMQ’s plugins carry AMQP 1.0, MQTT and STOMP over WebSocket, each as a distinct plugin from its raw-TCP counterpart. The wire framing carried inside the WebSocket connection is unchanged; what changes is the transport underneath it, which matters specifically for browser-based clients, where an application has no way to open a raw TCP socket to a broker and a WebSocket connection is the only option reachable from JavaScript running in a page.
XMPP, an XML-based chat protocol that is not covered above, is also used to connect AI assistants to chat clients; see XMPP Bots.
JMS / Jakarta Messaging: an API, not a wire protocol
JMS — now Jakarta Messaging under the Jakarta EE umbrella, the same specification continued under a new
name after the Java EE-to-Jakarta EE transfer — is conspicuously different in kind from everything above: it is
a Java API specification, defining interfaces such as ConnectionFactory, Destination, MessageProducer
and MessageConsumer, and says nothing at all about what goes on the wire between a JMS client and a broker.
A JMS provider is the library that implements those interfaces for a specific broker, and it is the provider,
not the specification, that decides which of the protocols described above — or a broker-specific protocol of
its own — actually carries the bytes. A RabbitMQ JMS provider implements the JMS interfaces on top of an AMQP
0-9-1 client underneath; a JMS bridge in front of Kafka implements the same interfaces on top of the Kafka
binary protocol; ActiveMQ Artemis can serve JMS clients over its own native protocol or over AMQP 1.0. Two
applications that both "use JMS" are, in general, using two entirely different wire protocols underneath, and
cannot talk to each other directly without a bridge between those two protocols — the API uniformity is a
portability convenience for the application code that calls it, not evidence of any shared wire format.
@Component
class OrderNotifier {
private final JmsTemplate jmsTemplate;
OrderNotifier(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
void notifyOrderCreated(String orderId) {
jmsTemplate.convertAndSend("orders", orderId);
}
}
This code is identical whether spring.jms.* auto-configuration wires up an Artemis ConnectionFactory talking
Artemis’s own protocol, or a JMS-over-Kafka bridge implementation — JmsTemplate is programmed entirely
against the Jakarta Messaging API, never against a wire protocol directly. See
Spring Boot Reference Documentation — JMS for how Spring Boot auto-configures a ConnectionFactory and JmsTemplate for a given provider.
Protocol support: Kafka vs. RabbitMQ
The table and figure below summarize which of the protocols on this page each broker actually speaks on the wire, checked directly against each project’s own protocol documentation rather than either vendor’s marketing comparison of the other.
| Protocol | Apache Kafka | RabbitMQ |
|---|---|---|
AMQP 0-9-1 |
Not supported |
Native — RabbitMQ’s original protocol |
AMQP 1.0 |
Not supported |
Native (core since RabbitMQ 4.0; a plugin in earlier releases) |
Kafka protocol |
Native — the only protocol Kafka brokers accept |
Not supported |
MQTT |
Not supported natively (unofficial third-party bridges exist, not part of the project) |
Plugin (versions 3.1, 3.1.1, 5.0) |
STOMP |
Not supported |
Plugin |
RabbitMQ Stream protocol |
Not applicable — a RabbitMQ-specific protocol |
Native, for the Streams feature; port 5552 (5551 with TLS) |
WebSocket transport |
Not applicable — the Kafka protocol has no WebSocket variant |
Plugins carry AMQP 1.0, MQTT and STOMP over WebSocket |
JMS / Jakarta Messaging |
Not native — only reachable via a third-party JMS provider bridging onto the Kafka protocol |
Reachable via a JMS provider implemented on top of the native AMQP 0-9-1 client |
Connection hierarchies: AMQP 0-9-1 vs. AMQP 1.0
The two protocols' structural models, side by side — a single TCP connection multiplexing channels that carry class/method calls directly, against a single TCP connection multiplexing sessions that each hold one or more directional links governed by credit-based flow control: