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

Grid comparing protocol support between Apache Kafka and RabbitMQ: AMQP 0-9-1 and AMQP 1.0 are native to RabbitMQ and unsupported by Kafka; the Kafka protocol is native and exclusive to Kafka; MQTT and STOMP are RabbitMQ plugins and unsupported by Kafka; the RabbitMQ Stream protocol is native to RabbitMQ and not applicable to Kafka; WebSocket transport is available via RabbitMQ plugins and not applicable to Kafka; JMS is an API layered over each broker’s native protocol rather than natively supported by either

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:

flowchart TD subgraph Amqp091["AMQP 0-9-1"] A1["TCP Connection"] --> B1["Channel 1"] A1 --> B2["Channel 2"] A1 --> B3["Channel N"] B1 --> C1["basic.publish\n(class: basic)"] B1 --> C2["exchange.declare\n(class: exchange)"] B2 --> C3["queue.bind\n(class: queue)"] end subgraph Amqp10["AMQP 1.0"] D1["TCP Connection\n(Container to Container)"] --> E1["Session 1"] D1 --> E2["Session 2"] E1 --> F1["Link: sender\n(source to target)"] E1 --> F2["Link: receiver\n(source to target)"] F2 -->|"credit-based flow"| G1["Delivery outcome:\nAccepted / Rejected /\nReleased / Modified"] end