Kafka and RabbitMQ both move messages, but they solve different problems, and picking the wrong one shows up in your on-call rotation later.
Every few months a team asks me to settle a Kafka-versus-RabbitMQ argument, and the argument is almost always the wrong shape. People compare them on throughput numbers or on how many nines the vendor claims. The real difference is architectural, and once you see it the choice usually makes itself.
RabbitMQ is a traditional message broker. Producers publish to exchanges, the broker routes each message into one or more queues based on rules you define, and consumers receive messages pushed to them. When a consumer acknowledges a message, the broker deletes it. The broker is smart: it holds the routing logic, tracks per-message state, and decides who gets what. Consumers stay thin.
Kafka is a distributed, append-only commit log. Producers append records to a topic, which is split into partitions, and those records stay on disk for a retention window whether or not anyone has read them. Consumers pull records and track their own position with an offset. Kafka does not route or delete on your behalf. The broker is deliberately dumb, and the consumers carry the intelligence.
That single distinction, smart-broker-thin-consumer versus dumb-broker-smart-consumer, drives nearly every other trade-off below.
Partitions are Kafka's unit of parallelism. A topic with 24 partitions can be read by up to 24 consumers in a group, each owning a slice, and because a partition is just a sequential file append, Kafka pushes enormous volume. Hundreds of thousands to millions of records per second on modest hardware is normal.
RabbitMQ trades raw throughput for routing power. Its exchanges (direct, topic, fanout, headers) let you express routing that would be awkward to bolt onto Kafka. Send a message to every queue matching orders.*.eu, dead-letter it after three failures, set a per-message TTL: this is native.
# RabbitMQ: topic exchange routing by key
channel.exchange_declare(exchange="orders", exchange_type="topic")
channel.queue_bind(exchange="orders", queue="eu_fulfilment",
routing_key="orders.*.eu")
channel.basic_publish(
exchange="orders",
routing_key="orders.created.eu",
body=payload,
properties=pika.BasicProperties(delivery_mode=2), # persistent
)
In Kafka you would push that routing decision into producer partitioning or into consumer-side filtering, because the broker will not do it for you.
Kafka guarantees order within a partition. Records keyed the same way land on the same partition and are read in the sequence they were written, which is why event sourcing and change-data-capture pipelines lean on it. Retention is the other headline feature: records live for a configured time or size, so a new consumer can start at offset zero and reprocess history, and a broken consumer can rewind after you ship a fix.
# Kafka topic: keep 7 days, replayable by any new consumer group
retention.ms=604800000
cleanup.policy=delete
RabbitMQ has no equivalent replay in its classic model. A message exists until it is acknowledged, then it is gone. Ordering holds within a single queue with a single consumer, but the moment you scale to competing consumers, strict order goes out the window. If your requirement is "reprocess last Tuesday's events against new code," RabbitMQ is the wrong tool.
Both systems fan work out, but the semantics differ. RabbitMQ uses competing consumers: several workers attach to one queue and the broker hands each message to whichever is free. It is load balancing, and it is excellent for task queues where any worker can handle any job.
Kafka uses consumer groups: within a group each partition is owned by exactly one consumer, so parallelism is capped by partition count, and every group reads the full topic independently. The billing service and the analytics service can each consume the same events without stepping on each other. That independent, replayable fan-out is something RabbitMQ does not model cleanly.
Both default to at-least-once, so consumers must be idempotent either way. Kafka can reach effective exactly-once within its own boundary using idempotent producers and transactions, which matters for stream processing that reads and writes Kafka. RabbitMQ leans on publisher confirms and consumer acks, and gives you fine control over redelivery and dead-lettering. Neither gives you free exactly-once across an external database. Design for duplicates regardless.
Kafka: heavier to run. You are operating a partitioned, replicated distributed system. Even with KRaft removing the old ZooKeeper dependency, you still reason about partition counts, replication factors, consumer lag, rebalances, and disk sizing for retention. It rewards teams with real platform capacity.
RabbitMQ: lighter to start and simpler to reason about for classic workloads. A single node or a small cluster gets you far. Clustering and quorum queues add nuance under load, but the day-one operational surface is smaller.
If you want the wider context on how these brokers fit into a system, our event-driven architecture guide covers the surrounding design, and message queue patterns digs into the delivery and retry patterns both brokers rely on.
Reach for Kafka when you have event streaming, log and metrics aggregation, analytics and ETL pipelines, or event sourcing where replay and ordered history are the point. High, sustained volume with multiple independent consumers is its home turf.
Reach for RabbitMQ when you have task and job queues, request/response and RPC flows, complex conditional routing, or lower-volume workloads where per-message control beats raw throughput. It is the pragmatic choice for background jobs behind a web app.
Ask three questions. Do consumers need to replay history or read the same stream independently? That points to Kafka. Do you need rich routing, per-message TTL, priorities, or straightforward competing-consumer work distribution? That points to RabbitMQ. And honestly, what can your team operate? A broker you cannot debug at 3 a.m. is a liability no benchmark makes up for.
For a product moving events between services at scale, feeding analytics, and needing replay, we default to Kafka and accept the operational tax. For background jobs, email and webhook processing, and anything that looks like a work queue with interesting routing, we default to RabbitMQ and spend the saved complexity budget elsewhere. Plenty of mature systems run both, Kafka as the event backbone and RabbitMQ for task dispatch, because they were never really competitors. They are different tools that happen to share a category.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A good API is a promise you can keep for years. This is the map: the conventions, the protocols, and the details that make an API pleasant to use and safe to change.
A practical tour of the message queue patterns that keep distributed backends decoupled, resilient, and able to survive traffic spikes.
Explore more articles in this category
A practitioner's tour of where WebAssembly earns its keep in 2026, from browser apps to edge compute, plus the places it still doesn't fit.
A practical look at why Go usually outruns Python at runtime, where Python holds its own, and how to pick per workload.
A grounded look at WebAssembly, the portable binary format that runs code at near-native speed inside a secure sandbox.
Evergreen posts worth revisiting.