0%
RabbitMQ Deep Dive: AMQP, Queues, and Delivery Guarantees
The Broker Model
Queues and Storage
Delivery Guarantees
Flow and Scale
RabbitMQ versus Kafka, Choosing a Broker
Most comparisons of these two systems are lists of features. The useful comparison is one structural difference, from which almost every feature difference follows.
Where the read position lives
One difference, and the root of the rest.
In RabbitMQ, the broker owns the read position. A message is delivered, acknowledged and removed, and the queue's contents are the set of messages not yet acknowledged. There is no concept of a position in a queue because the queue has no addressable sequence: it has a head, and consuming is removal.
In Kafka, the consumer owns the read position. The log is an immutable sequence of records with monotonic offsets, consuming is reading at an offset, and the consumer records where it got to. Nothing is removed by reading, and retention is a policy about time or size rather than about delivery. The broker does not know and does not need to know which records a consumer has processed.
Everything downstream follows from this. Replay exists in Kafka because the records are still there and the offset can be moved; it does not exist in RabbitMQ because consuming destroyed the message. Per-message routing exists in RabbitMQ because the broker inspects each message to decide where it goes; it does not exist in Kafka because the broker appends bytes to a partition and never looks inside them.
What each one's broker does per message
A cost comparison that explains the throughput difference.
A RabbitMQ broker, per message, parses the AMQP frame, evaluates the exchange's routing against its bindings, enqueues into one or more queues, tracks the message's delivery state per consumer, processes the acknowledgement and removes it. That is a meaningful amount of per-message work, and it is what buys per-message routing and per-message acknowledgement.
A Kafka broker, per batch, validates the batch, appends it to the active segment's file and updates the index sparsely. It does not inspect individual records, does not track per-consumer delivery state, and does not process per-message acknowledgements. Kafka's throughput advantage is not an optimisation of the same work; it is the absence of most of the work, which is why the gap is a factor rather than a percentage.
The corollary is the one that matters when choosing. Kafka's design makes a million messages per second achievable on modest hardware and makes "deliver this message to the one consumer that handles orders for customer 4821" impossible to express. RabbitMQ's design does the opposite. Neither is a limitation to be worked around; each is the price of the other's capability.
What the unit of storage is in each
A difference in granularity that explains several operational surprises.
RabbitMQ's unit is the message. A message is an object the broker tracks individually, with its own delivery state, its own position relative to other messages and its own routing outcome, and the broker can act on it alone: expire it, reorder it, dead-letter it, redeliver it. That individual tracking is what every per-message feature in the course depended on, and it is also a per-message memory and bookkeeping cost.
Kafka's unit is the segment, a file of appended batches with a sparse index, and nothing below the batch is individually addressable by the broker. Retention therefore deletes whole segments rather than messages, which is why a Kafka retention policy expires data in steps rather than continuously and why a single record cannot be removed without rewriting or compacting.
The operational consequence is that a RabbitMQ queue's size is the sum of its messages and a Kafka partition's size is the sum of its segments, so a partition holding one record still holds at least one segment file. On a cluster with tens of thousands of partitions that floor is a real number, which is the structural reason partition count is a capacity decision rather than a free parameter.
Where the delivery guarantee actually comes from
The same words, two different mechanisms, which is why the guarantees do not compose.
RabbitMQ's at-least-once comes from the acknowledgement: the broker keeps the message until a consumer acknowledges, and a consumer that dies without acknowledging causes a redelivery. The guarantee is per message and the broker enforces it, which is why it holds even when the consumer is a script someone wrote in an afternoon.
Kafka's at-least-once comes from the offset commit: the consumer reads, processes and then commits, and a consumer that dies before committing re-reads from the last committed offset. The guarantee is per partition position and the consumer enforces it, which means a consumer that commits in the wrong order silently converts at-least-once into at-most-once with nothing on the broker side to prevent it.
That asymmetry is worth internalising because it changes where the review attention goes. On RabbitMQ, the delivery guarantee is mostly a configuration property and the review question is whether acknowledgement happens after the work. On Kafka it is mostly a code property, and the review question is where the commit sits relative to the side effects, which is a harder thing to verify and a much easier thing to get wrong.
The ordering guarantees, which are not comparable
Two different things both called ordering.
Kafka guarantees order within a partition, absolutely, for all time, because order is the log's identity rather than a property of delivery. A consumer reading a partition sees records in the order they were appended, and a replay a year later sees the same order. The cost is that the partition is the unit of parallelism, so ordering and concurrency trade against each other at a fixed exchange rate: one consumer per partition per group.
RabbitMQ guarantees that a single queue delivers to a single consumer in the order messages entered the queue, which is a weaker statement in two ways. A redelivery after a nack or a consumer failure reinserts the message, usually at the head but without a guarantee relative to messages published since. And with multiple consumers on one queue, order is lost by design, because the whole point of multiple consumers is to process concurrently.
The practical difference is that Kafka's ordering survives failure and RabbitMQ's does not, because Kafka's order is a property of the stored data and RabbitMQ's is a property of a delivery sequence that a failure interrupts. A system that needs per-key ordering through failures wants Kafka's partitions or RabbitMQ's single-active-consumer, and the latter costs the concurrency entirely.
What each one's client has to do
A division of labour that shows up in library size and in how much can go wrong.
A RabbitMQ client opens a connection, opens channels, declares topology, publishes with a routing key and consumes with an acknowledgement mode. It holds almost no state: the broker knows which messages are outstanding, which consumer has what, and where every queue is, so a client that reconnects asks for nothing and resumes.
A Kafka client holds the group membership, the partition assignment, the fetch positions, the commit state and the coordinator connection, and it participates in a rebalance protocol. Most of Kafka's apparent complexity lives in the client rather than in the broker, which is why a Kafka consumer group has failure modes with no RabbitMQ analogue: a rebalance storm, a consumer evicted for a slow poll loop, an assignment that changed mid-processing.
The practical reading is that a RabbitMQ client is easy to write badly and hard to write catastrophically, while a Kafka client is usually a well-tested library doing something intricate on your behalf. That is why RabbitMQ's common failures are configuration and Kafka's are often the consumer loop's shape, and it is worth knowing before attributing a problem to the broker.
What fan-out means in each
The same word, two implementations with different costs.
In RabbitMQ, fan-out is a topology: an exchange with three bound queues copies each message into three queues, each with its own consumers and its own acknowledgement state. Adding a fourth consumer group is adding a queue and a binding, with no change to the publisher, and the cost is that the message is stored once per queue, so three subscribers means three times the storage.
In Kafka, fan-out is free. The log is stored once and every consumer group reads it independently with its own offsets, so adding a tenth consumer group costs ten reads of the same bytes, mostly from page cache, and no additional storage at all. That asymmetry is the strongest single argument for Kafka in a system with many independent consumers of the same data, and it gets stronger with every consumer added.
The reverse asymmetry exists too. Removing one subscriber in RabbitMQ deletes a queue and the messages stop being copied; in Kafka a consumer group that stops reading leaves the data in place, consuming retention, and nothing about the system notices that nobody wants it. A dozen abandoned consumer groups is a Kafka-specific form of mess.
The broker owns the read position in RabbitMQ and the consumer owns it in Kafka, and nearly every other difference follows: replay and free fan-out on one side, per-message routing and per-message acknowledgement on the other. Kafka's throughput advantage is the absence of per-message work rather than a faster version of it. Kafka's ordering survives failure because it is a property of the stored log; RabbitMQ's is a property of a delivery sequence.