ulearn/systems

topics / async-messaging

RabbitMQ vs Kafka

Smart broker vs. dumb log — not a benchmark, a difference in what each one actually does at runtime. Watch the same published event get pushed to a worker and deleted on ack on one side, while it sits in a log getting pulled by independent consumer groups on the other. Then use Match to score a workload's traits, or Study for the full scenario-by-scenario reasoning.

Two different ideas of what a broker is

RabbitMQ and Kafka both move messages from producers to consumers, both scale horizontally, and both show up on the same “message queue” shortlist — which makes it easy to treat them as interchangeable and pick whichever one a teammate already knows. They're not interchangeable. They're built on two genuinely different mental models, and most of the “which one should we use” friction people hit in production traces back to picking one and expecting the other one's behavior.

RabbitMQ is a smart broker. A message arrives at an exchange, and the broker itself decides — based on routing rules you configured — which queue(s), if any, it goes to. The broker is doing real work: evaluating bindings, applying routing keys, tracking per-message acknowledgment, deciding when to redeliver. Once a message is acked by every queue it was routed to, it's gone. RabbitMQ implements AMQP 0-9-1, the same protocol whether you're running one queue or a cluster of them.

Kafka is a dumb, fast log. A message is appended to a partition— an ordered, append-only file, replicated across brokers — and it just sits there until its retention window expires, whether or not anyone's read it. Kafka does almost no per-message work: no routing decisions, no per-message ack. Consumers do the work instead, tracking their own position (an offset) in the log and moving it forward as they read. “Dumb” here is a compliment, not a knock — offloading intelligence out of the broker and into consumers is exactly what lets a Kafka cluster push far more raw throughput than a broker that has to make a routing decision on every single message.

Everything else in this page — retention, ordering, routing, ecosystem, ops complexity — is a direct consequence of that one architectural choice. Learn that choice and the rest stops being a list of trivia to memorize.

RabbitMQ's building blocks

Exchange

Where a producer actually publishes to — never straight to a queue. An exchange has a type (direct, topic, fanout, headers) that decides how it matches messages to bindings. This is where RabbitMQ's routing intelligence lives.

Queue & binding

A queue is a durable, ordered backlog; a binding connects an exchange to a queue, optionally with a routing-key pattern (orders.eu.*). A message can fan out to several queues if several bindings match it.

Ack & prefetch

A consumer explicitly acks a message once it's actually done processing it — not just received it. An unacked message whose consumer dies gets redelivered. Prefetchcaps how many unacked messages one consumer can hold at once, so one slow worker can't hoard the whole backlog.

Once it's acked, it's gone

A classic or quorum queue deletes a message the moment every bound queue has acked it. There's no built-in replay. (RabbitMQ's newer streamsfeature is a log-like queue type that can replay — but that's an opt-in exception, not how a default queue behaves.)

Kafka's building blocks

Topic & partition

A topic is a named stream, split into partitions for horizontal scale. Each partition is its own ordered, append-only log, replicated across brokers for durability. Order is only guaranteed within a partition, never across the whole topic.

Partition key

A producer picks a partition per message — usually by hashing a key (a user id, an order id). Same key, same partition, every time, which is what makes “ordered per key” a free property rather than something you build.

Consumer group & offset

Consumers in the same group split a topic's partitions between them — each partition goes to exactly one consumer in the group at a time, capping useful parallelism at the partition count. Each group tracks its own offset per partition, independently of every other group reading the same topic.

Retention

A message stays for a configured window (time or size based, or forever for a compacted topic keyed by id) regardless of whether it's been consumed. Replay is just resetting a consumer group's offset backward — no special feature required.

Six questions that actually decide it

Skip the reputations (“Kafka is the scalable one,” “RabbitMQ is the simple one”) and ask what the workload actually needs. These are the six traits the matcher on this topic's Match tab scores — walk through them for your own system before reaching for either one.

  1. Will a consumer ever need to replay history? If yes, you need a durable log with retention independent of consumption — that's Kafka's default behavior, not RabbitMQ's.
  2. Does routing depend on message content?If a message's destination depends on more than a partition key — tenant, region, event subtype — RabbitMQ's exchanges express that declaratively. Kafka makes every consumer read everything and filter.
  3. What's the actual volume? A single well-run RabbitMQ queue (or a few, sharded by hand) is genuinely fine for moderate traffic. Reach for a partitioned log when one queue would become the bottleneck, not by default.
  4. What ordering do you actually need? “None” is neutral. “Per key” favors Kafka — it falls out of partitioning for free. “Strict and global” is expensive in both: one partition, or one queue with one consumer. Neither gives you strict order and full parallelism at once.
  5. Who's consuming — one pool, or several independent systems?A worker pool sharing one backlog is RabbitMQ's competing-consumers pattern. Several independent systems each needing the full stream, at their own pace, is what consumer groups are for.
  6. Once it's processed, is the data done, or is it a system of record? If the data needs to outlive being read — for audit, reprocessing, or because the log isthe source of truth — that's Kafka's job description, not a queue's.

Side-by-side

 RabbitMQKafka
Core modelSmart broker — exchanges route, queues holdDumb log — partitioned, replicated, append-only
Delivery unitMessage, explicitly ackedLog entry, offset committed
After it's consumedDeleted once acked (streams excepted)Kept until retention expires, regardless
ReplayNot by defaultReset a consumer group's offset
OrderingPer queue, best-effort under concurrencyStrict within a partition, by key
Routing logicExchanges: direct/topic/fanout/headersPartition key only — filter client-side otherwise
Multiple independent consumersOne queue (one full copy) per consumerOne consumer group per consumer, same log
Parallelism ceilingAdd queues/consumers freelyCapped by partition count per group
Typical scaleTens of thousands of msg/s per queueMillions of msg/s across a partitioned cluster
Request/replyNative pattern (reply-to + correlation id)Not a primitive — build it on two topics
Stream processing ecosystemPlugin-based, narrowerKafka Streams, ksqlDB, Connect, wide ecosystem
Operational footprintSimpler at moderate scaleHeavier — partitions, replicas, retention tuning

Scenario playbook

Nine concrete workloads, each with the mechanism-level reason one architecture fits and the specific friction you'd hit picking the other — including one deliberate toss-up, because not every decision should resolve to a clean winner. Open the Match tab and click any of these to load its exact traits into the live scoring model.

Background job queue

RabbitMQ

A web app hands off discrete units of work — resize an image, send an email, generate a PDF — to a pool of workers, each job done exactly once.

Why: This is the textbook competing-consumers pattern, and RabbitMQ's queue model is built for exactly it: a worker pulls a job, gets a per-message ack once it's actually done, and a prefetch limit stops one greedy worker from hoarding jobs the others could be doing. If a worker crashes mid-job, the unacked message just goes back in the queue for someone else — no extra plumbing required.

The other one, here: Kafka can technically move the messages, but you'd be rebuilding RabbitMQ's ack/retry/prefetch machinery yourself on top of offset commits, which only tell you "we're past message N," not "job N actually finished." You'd also be capping your worker pool at the partition count for no reason — this workload never needed partitioning in the first place.

Synchronous-feeling RPC between two services

RabbitMQ

Service A publishes a request and needs a specific response back — over a broker instead of a raw HTTP call, so a slow or restarting Service B doesn't take Service A down with it.

Why: RabbitMQ has a first-class pattern for this: Service A publishes with a `reply-to` queue and a `correlation-id`, Service B replies onto that queue, and Service A matches the correlation id back to the waiting caller. It's a direct, one-off message with a direct, one-off reply.

The other one, here: Kafka has no request/reply primitive — you'd fake it with a request topic, a response topic, and your own correlation-id bookkeeping to match replies to callers, on infrastructure whose entire design point is durable streaming, not point-to-point round trips.

Conditional, content-based event routing

RabbitMQ

An event needs to reach different destinations depending on its own content — which tenant it belongs to, which region, which subtype — not a flat "give it to everyone" or "give it to whoever's free."

Why: This is what exchanges and bindings are for. A topic exchange with bindings like `orders.eu.*` and `orders.*.created` lets the broker itself decide where a message goes, based on a routing key — the routing logic lives in configuration, not in every consumer.

The other one, here: Kafka only routes by partition key. Content-based delivery means every consumer subscribes to the whole topic and filters client-side, or you pre-split the data into many topics up front and hope your routing needs never change shape.

High-throughput clickstream / user-event analytics

Kafka

Millions of small events a day, feeding a real-time dashboard, a data warehouse loader, and an ML feature pipeline — three independent systems, same stream.

Why: Kafka partitions the topic across brokers to absorb the volume, keys events by user id so each user's events land on the same partition in order, and lets the dashboard, the warehouse loader, and the ML pipeline each run as their own consumer group, reading at their own pace off the same durable log. When the ML pipeline needs to backfill a feature over the last 30 days, that's just resetting its group's offset.

The other one, here: RabbitMQ would need three separate queues bound to the same exchange — three full copies of a multi-million-event-a-day stream — and none of them could be replayed for a fourth consumer that shows up next quarter. The backfill case alone rules it out.

Event sourcing / CQRS with a full audit trail

Kafka

The event log itself is the source of truth. Read models (a search index, a reporting table, a cache) are just projections, rebuilt at any time by replaying the log from the start.

Why: Kafka's log is durable by default and doesn't forget a message once it's read — which is the entire premise of event sourcing: the log is the system of record, and every read model is disposable and rebuildable. Partitioning by aggregate id (an order id, an account id) gives strict per-aggregate ordering, which is the one ordering guarantee event sourcing actually needs.

The other one, here: A RabbitMQ queue deletes a message the instant it's acked. The log can never be the source of truth if it forgets its own history — you'd end up bolting on a separate event store next to RabbitMQ, at which point RabbitMQ is just an unnecessary extra hop in front of the thing that's actually doing the job.

Centralized log / metrics pipeline

Kafka

Every service ships logs and metrics into one pipeline; alerting, a search index, and a data-lake archive all need the same firehose, independently, and a new sink might get added next month.

Why: This is Kafka's original use case at LinkedIn, almost unchanged: a high-volume firehose, several independent sinks (Kafka Connect can push to Elasticsearch, S3, a warehouse) each as their own consumer group, and retention long enough that a sink that falls behind — or a brand new one added later — can catch up from history instead of only from "now on."

The other one, here: RabbitMQ fan-out means one queue, and one full copy of the firehose, per sink — expensive at this volume — and a sink that's down for an hour doesn't get to catch up on the hour it missed once its queue's retention or capacity limit is hit. There's no underlying log to rewind.

Financial transaction ledger

Kafka

Every debit and credit for a given account must be applied in exact order, and the full history has to be inspectable and replayable for reconciliation and audits, years later.

Why: Partitioning by account id gives every account's transactions strict, in-order processing without forcing the whole ledger through one queue. Long retention (or a compacted topic keyed by account) means the ledger processor, the audit tooling, and a reconciliation job can each read the same authoritative history independently, at any time.

The other one, here: RabbitMQ can give you strict order too — but only per queue, which means one queue per account to avoid serializing every account behind every other one, which isn't operationally realistic at any real scale. And once a transaction is acked and gone, there's no durable history left for an audit to replay.

IoT / sensor telemetry from a large device fleet

Kafka

Tens of thousands of devices continuously streaming small readings, feeding real-time alerting, long-term storage, and a model-training pipeline.

Why: The device count alone is a partitioning problem — Kafka scales the topic horizontally across brokers and keys by device id, so each device's readings stay ordered without a single queue having to absorb the entire fleet. Alerting, storage, and model training run as independent consumer groups off the same log.

The other one, here: A single RabbitMQ queue tops out well below this volume, and scaling it means sharding queues by hand with no built-in mechanism for it. The fan-out-to-three-independent-systems requirement runs into the same full-copy-per-consumer cost as the log-aggregation case above.

Real-time chat / push notification fan-out

Either works

A message posted in a room needs to reach every currently-connected client immediately. Nobody needs yesterday's messages replayed through this pipe — that's what the chat history database is for, not the delivery layer.

Why: This one is a genuine toss-up, and that's the point: it's simple, low-stakes fan-out with no ordering, no replay and no durability requirement — neither architecture's distinguishing strength is even being tested. RabbitMQ's fanout exchange delivers to every bound queue with almost no configuration. Kafka would work too, at the cost of infrastructure this workload doesn't need.

The trap: The mistake isn't picking either one — it's picking Kafka here out of habit ("it's the scalable one") and inheriting operational weight this traffic never asked for, or picking RabbitMQ and assuming it'll be trivial to bolt on replay/analytics later, when that's precisely the thing it doesn't do. Let the actual requirement decide, not the reputation.