Kafka vs RabbitMQ — Two Message Brokers, Fundamentally Different Mental Models
Kafka and RabbitMQ solve different problems. Pick the wrong one and you'll either miss replay when you need it, or run a cluster for work that a queue handles fine.
Kafka vs RabbitMQ — Two Message Brokers, Fundamentally Different Mental Models
Your team decides to decouple services with a message broker. Someone suggests Kafka because "it scales." Someone else says RabbitMQ because "it's simpler." You pick one, build around it, and six months later you're either explaining why you can't replay last week's events (you chose RabbitMQ for event sourcing), or you're maintaining a 20-topic Kafka cluster to process three types of background jobs.
Both are excellent. Both are genuinely production-proven at scale. But they solve different problems, and using the wrong one means fighting your infrastructure instead of shipping.
Quick Decision Matrix
| If you need... | Choose |
|---|---|
| Process a job once, by exactly one worker | RabbitMQ |
| Route messages by content (topic patterns, headers) | RabbitMQ |
| Request/reply over async transport | RabbitMQ |
| Per-message TTL or priority queues | RabbitMQ |
| Multiple independent services consuming the same events | Kafka |
| Replay historical events after a bug fix | Kafka |
| Very high sustained aggregate throughput | Kafka |
| Event sourcing / CQRS | Kafka |
| Fan-out to analytics + operational systems simultaneously | Kafka |
| Simple background jobs at modest scale | RabbitMQ |
30-Second Summary
RabbitMQ is a general-purpose message broker implementing AMQP. Producers send to exchanges; exchanges route to queues via binding rules; consumers receive and acknowledge. The broker tracks delivery state. Once a consumer ACKs, the message is deleted. It's operationally simple at small-to-medium scale, has sophisticated routing primitives, and excels at work queues and RPC patterns. The downside: no replay and no native fan-out to independent consumers.
Apache Kafka is a distributed commit log. Producers write to topic partitions; consumers pull at their own pace and manage their own read position (offset). Messages are retained on disk for days or weeks by default. Any number of independent consumer groups can read the same topic independently, including from the very beginning. Kafka 4.0 (March 2025) removed ZooKeeper entirely — KRaft is now the only cluster mode, which meaningfully reduces operational overhead. The current release line is 4.3.
Core Architecture: The Fundamental Difference
The divide is broker-centric vs. log-centric.
RabbitMQ is the "smart broker, dumb consumer" model. The broker does the work: routing decisions, delivery tracking, retries, TTL enforcement. Consumers are simple — they receive, process, and ACK. This makes consumer code straightforward, but the broker is both the source of truth and the bottleneck.
Kafka is the "dumb broker, smart consumer" model. The broker stores an ordered, append-only log per partition. Consumers decide when to read, track their own position, and can read any partition at any speed. The broker doesn't know or care whether a consumer group is current or 2 million messages behind.
The practical consequence: in RabbitMQ, adding a new downstream service that needs the same events requires a new queue bound to a fanout exchange — and it only receives messages from the moment it was created. In Kafka, a new consumer group can start from offset 0 and replay every event ever written to that topic.
# RabbitMQ: classic work queue — each message goes to exactly one worker
channel.basic_qos(prefetch_count=1) # one unacked message per consumer at a time
def process_task(ch, method, properties, body):
task = json.loads(body)
do_work(task)
ch.basic_ack(delivery_tag=method.delivery_tag) # message deleted on ACK
channel.basic_consume(queue='email-jobs', on_message_callback=process_task)
channel.start_consuming()# Kafka: two independent consumer groups reading the same topic.
# Neither affects the other — they manage their own offsets.
# Group A: fulfillment service (reads from current position)
fulfillment = KafkaConsumer(
'orders',
group_id='fulfillment-service',
bootstrap_servers=['kafka:9092'],
auto_offset_reset='latest'
)
# Group B: analytics pipeline (can replay from the very start)
analytics = KafkaConsumer(
'orders',
group_id='analytics-pipeline',
bootstrap_servers=['kafka:9092'],
auto_offset_reset='earliest' # reads all historical events
)Performance: What the Numbers Actually Say
For most applications under ~10K messages/second, performance is a non-issue — either broker handles it comfortably. Above that, be careful which numbers you're repeating, because the benchmark everyone cites has some serious asterisks.
The Confluent benchmark ("Benchmarking Apache Kafka, Apache Pulsar, and RabbitMQ", August 2020) is the source of nearly every figure in circulation. What it actually reported:
| Peak throughput | p99 latency | |
|---|---|---|
| Kafka | 605 MB/s | 5 ms at 200 MB/s load |
| RabbitMQ (mirrored queues) | 38 MB/s | 1 ms — but only at a reduced 30 MB/s load |
That's the origin of "Kafka writes 15x faster than RabbitMQ," which is a direct quote from the post. It's also the origin of two claims that the post does not support: Kafka's latency is 5 ms at p99, not "10–50ms on average," and the p99.9 tail-latency finding in that benchmark is Kafka versus Pulsar, not versus RabbitMQ. RabbitMQ was never measured at p99.9 at equal throughput. The benchmark's actual conclusion about RabbitMQ is the reverse: it "achieves the lowest latency among the three systems, but only at a much lower throughput."
The asterisks matter more than the numbers:
- It was published by Confluent, who sell Kafka, with no conflict-of-interest disclosure.
- It tested Kafka 2.6.0 and RabbitMQ 3.8.5 — both six years old now.
- RabbitMQ ran classic mirrored queues, which the post says it chose for performance reasons and which no longer exist — mirroring was removed in RabbitMQ 4.0.
- RabbitMQ ran with persistence disabled and consumer auto-ack, i.e. a weaker durability contract than Kafka's.
- StreamNative (who sell Pulsar) published a rebuttal arguing the durability settings weren't matched. They also sell a competing product.
For RabbitMQ figures, prefer RabbitMQ's own 2024 AMQP benchmarks, which are far more favourable than the numbers usually attributed to it: on a single 8-core Intel NUC with 12-byte messages, they measured 99,413 msg/s on a classic queue and 83,459 msg/s on a quorum queue with raised flow-control settings. The same test hit only 9,986 msg/s with default settings — and Team RabbitMQ attribute that directly to slow fsync on their test box, not to a RabbitMQ ceiling. Their own guidance is worth quoting: RabbitMQ "uses conservative flow control default settings to favour stability in production over winning performance benchmarks."
The honest summary: log-structured brokers scale aggregate throughput far higher; per-queue brokers win latency at modest rates. That architectural conclusion has held up for a decade. The specific numbers have not.
Horizontal scaling tells the same story. Kafka scales near-linearly: add brokers, add partitions, consumers rebalance automatically. RabbitMQ scaling is more involved — quorum queues carry Raft coordination overhead, and the classic mirrored queues that used to be the alternative are gone.
Slow consumer hazard in RabbitMQ — When a consumer falls behind, messages accumulate. Once a node crosses its memory threshold it raises an alarm and blocks all publishing connections across the cluster — not just the one responsible. In Kafka, a slow consumer group just falls behind in its offset with zero impact on the broker or other consumers. This asymmetry is brutal at scale.
Routing: Where RabbitMQ Has No Peer
This is where RabbitMQ genuinely wins. Four exchange types give you sophisticated routing out of the box:
- Direct: Exact key match —
payment.createdgoes to the payments queue only - Topic: Wildcard patterns —
payment.europe.*matchespayment.europe.high-valueandpayment.europe.chargeback - Fanout: Broadcast to every bound queue
- Headers: Route by message attribute values (
priority: high, region: eu)
Kafka has no equivalent. Messages route to partitions by key hash (for ordering guarantees) or round-robin. Content-based routing in Kafka means either topic proliferation or consumers receiving everything and discarding most of it.
The "Kafka can't do queues" line needs an update. KIP-932 "Queues for Kafka" — share groups — went production-ready in Kafka 4.2 (February 2026). Share groups give you per-record acknowledgement and delivery-attempt counting, letting many consumers cooperatively drain a topic without partition-count limiting your parallelism. Kafka's docs frame it exactly as the queue case: use share groups "where records are processed one at a time, rather than as part of an ordered stream."
This genuinely erodes one leg of the classic comparison. It does not give Kafka RabbitMQ's exchange routing, per-message TTL, or priority queues — so the decision framework below still holds. But if your only reason for running RabbitMQ alongside Kafka is competing consumers on a work queue, that reason is weaker than it was a year ago. Two practical notes: the __share_group_state internal topic defaults to replication factor 3, and 4.2.0 shipped a share-group deadlock (KAFKA-20505) fixed in 4.2.1 — so treat 4.2.1 as the floor.
# RabbitMQ: route payment events by region and type using topic exchange
channel.exchange_declare(exchange='payments', exchange_type='topic')
# Each queue captures a different routing pattern
channel.queue_bind(queue='eu-payments', exchange='payments', routing_key='payment.europe.*')
channel.queue_bind(queue='high-value', exchange='payments', routing_key='payment.*.high-value')
channel.queue_bind(queue='audit-log', exchange='payments', routing_key='payment.#') # all
# Produce
channel.basic_publish(
exchange='payments',
routing_key='payment.europe.high-value',
body=json.dumps(payment)
)
# This message lands in eu-payments, high-value, AND audit-log simultaneouslyMessage Ordering and Delivery Guarantees
RabbitMQ: FIFO within a single queue. With multiple competing consumers on the same queue, strict ordering across the consumer pool is lost — consumer A may finish message 5 before consumer B finishes message 3. Delivery semantics: at-most-once (auto-ACK) or at-least-once (manual ACK). Exactly-once requires application-level deduplication.
Kafka: Ordered within a partition. Partition by entity key (user_id, order_id) for per-entity ordering — all messages with the same key always land in the same partition, in order. Global ordering requires a single-partition topic, sacrificing parallelism.
# Kafka: exactly-once semantics via transactional producer API.
# Requires kafka-python >= 2.2.0 — the transactional producer (KIP-98) landed
# in April 2025 after a four-year gap in releases, so most tutorials predate it.
# On kafka-python 3.x, enable_idempotence defaults to True and is implied by
# setting transactional_id; it's spelled out here for clarity.
producer = KafkaProducer(
bootstrap_servers=['kafka:9092'],
transactional_id='order-processor-1', # unique per producer instance
enable_idempotence=True
)
producer.init_transactions()
try:
producer.begin_transaction()
# Both writes commit atomically or neither does
producer.send('order-events', key=b'order-123', value=order_data)
producer.send('inventory-updates', key=b'item-456', value=inventory_data)
producer.commit_transaction()
except Exception:
producer.abort_transaction()Partition key as ordering primitive — In Kafka, partition by entity ID when you need per-entity ordering. For most event-driven systems, per-entity ordering (all events for a given order, a given user) is all you actually need. Global topic ordering is rarely necessary and costs you parallelism.
Retention and Replay
This is arguably the most consequential difference for modern architectures.
RabbitMQ: Ephemeral by design. ACK means delete. There is no replay, no historical access, no "what did the system see last Tuesday." Dead-letter queues handle failures but aren't a history store. If you chose RabbitMQ and six months later need to bootstrap a new analytics service with historical order data — you don't have it.
Kafka: Messages are retained on disk by policy — 7 days by default, configurable by time or byte size. Log compaction mode retains only the latest value per key (useful for materialized views and event sourcing). You can seek to any offset at any time.
consumer = KafkaConsumer(
'order-events',
group_id='new-reporting-service',
bootstrap_servers=['kafka:9092']
)
# Wait for partition assignment, then rewind to the beginning
consumer.poll(timeout_ms=1000)
for partition in consumer.assignment():
consumer.seek_to_beginning(partition) # replay all historical events
for message in consumer:
build_report_from_event(message.value)Replay enables patterns that are simply unavailable in RabbitMQ: bootstrapping a new microservice with all historical data, re-processing a topic after fixing a consumer bug, temporal debugging of production incidents, and CQRS read model reconstruction.
Operational Complexity
RabbitMQ: Single-node setup is simple; the built-in management UI is excellent. Quorum queues, introduced in 3.8 (2019), are operationally cleaner than classic mirrored queues — which were deprecated in 3.9 and removed outright in 4.0. Note that quorum is not the protocol default: x-queue-type still defaults to classic unless you set default_queue_type per-vhost or node-wide. If you want replicated queues, you have to ask for them.
Recent releases have moved fast here. 4.1 (April 2025) offloaded quorum queue log reads to channels, improving consumer throughput and CPU core utilization. 4.2 (October 2025) made Khepri the default metadata store and improved fanout routing throughput. 4.3 (April 2026) shipped a new quorum queue state machine with strict priority queues, delayed retry and per-queue consumer timeouts, and removed Mnesia and classic queues v1 entirely — so read the upgrade notes rather than assuming a drop-in.
The main operational hazard is memory pressure, and it's worth understanding precisely because it's blunt: when a node crosses its memory or disk threshold it raises an alarm and blocks every publishing connection, cluster-wide, not just the one responsible. Consumer-only connections keep running. This is distinct from RabbitMQ's per-connection credit-based flow control, which is targeted; the memory alarm is, in Team RabbitMQ's own words, "not the targeted rate limiting of credit based flow control, but a sledgehammer."
Kafka: Historically complex due to ZooKeeper. That's resolved in Kafka 4.0 (March 2025) — ZooKeeper is gone, KRaft is the only option, and Kafka is now a self-contained system. Partition management, consumer group rebalancing, and offset lag monitoring still require dedicated tooling (Kafka UI, Conduktor, or similar). Consumer group rebalancing during rolling restarts causes temporary processing pauses — a real concern for latency-sensitive workloads. Schema Registry (for Avro/Protobuf) is a common additional dependency.
The operational gap has narrowed significantly with Kafka 4.0. But for teams without dedicated platform engineering, RabbitMQ remains simpler to run day-to-day.
When to Use RabbitMQ
- Background job queues: email sending, image processing, PDF generation, payments — anything that should be processed exactly once by one worker
- Complex content-based routing via topic exchange patterns or headers
- Request/reply over async transport (RPC pattern with
reply_toandcorrelation_id) - Per-message TTL, priority queues, or delayed delivery
- Microservice decoupling at small-to-medium scale where replay isn't a requirement
- Teams that need operational simplicity without dedicated Kafka expertise
- Integrating with legacy systems that speak AMQP
When to Use Kafka
- Multiple independent services consuming the same events (orders topic consumed by fulfillment, billing, notifications, and analytics — all independently)
- Any architecture where replaying historical events is required (new service bootstrapping, bug-fix reprocessing)
- Event sourcing or CQRS — Kafka is the durable, replayable event log
- Sustained aggregate throughput beyond what a queue-per-consumer broker handles economically
- CDC pipelines — Debezium captures DB changes, downstream systems consume at their own pace
- Real-time analytics with Kafka Streams or ksqlDB
- Log aggregation from microservices/containers at scale
When to Use Both
Common in larger systems. A platform might use RabbitMQ for transactional background jobs (send email, process refund) where simplicity and per-message routing matter, and Kafka for the event streaming layer where fulfillment, analytics, and audit services all need independent access to the same order events.
Both Celery (Python) and Sidekiq (Ruby) support RabbitMQ for job queues. Kafka is the default backbone for data engineering pipelines. Teams that run both tend to have a clear mental model: RabbitMQ is the job queue, Kafka is the event bus.
Architecture: How Requests Flow
Final Verdict
For new projects in 2026, the mental model is straightforward:
RabbitMQ when your messages are commands — things that need to happen exactly once by one worker. Email jobs, payment processing, background tasks. Don't reach for Kafka when a queue does the job; you'll pay operational complexity for nothing.
Kafka when your messages are facts — events that describe something that happened, which multiple systems may care about now or in the future. Order placed, user signed up, payment received. If you might want to replay it, audit it, or bootstrap a new service from it, use Kafka.
The most common regret: choosing Kafka because "it scales" for a use case that's a background job queue. The second most common regret: choosing RabbitMQ for event-driven architecture and discovering six months later that there's no way to replay history.
When in doubt — if the message is a fact you might want to look at again, use Kafka. If it's a command that should happen once and be forgotten, use RabbitMQ.
Comments (0)
No comments yet. Be the first to share your thoughts!