RabbitMQ vs Kafka: Delivered or Remembered?
A smart queue versus a replicated log: routing, per-message semantics and DLX on one side; replay, independent readers and partition ordering on the other — plus the framework-queue option the rivalry hides.
RabbitMQ versus Kafka is framed as a rivalry, which is the first mistake — they're different data structures wearing server costumes. RabbitMQ is a smart queue: it routes messages to consumers and forgets them once acknowledged. Kafka is a replicated log: it appends events and remembers them whether anyone reads or not. Almost every "which one?" confusion dissolves once you ask the underlying question: do I need my messages delivered, or do I need them remembered?
RabbitMQ: the delivery specialist
Rabbit's model: producers publish to exchanges, exchanges route to queues by bindings (direct, topic wildcards, fanout, headers), consumers ack messages, acked messages cease to exist. This buys things Kafka can't cheaply give you:
- Rich routing as configuration.
orders.eu.*to one queue,orders.#.refundto another, everything to the audit queue — declared, not coded. - Per-message everything: priorities, TTLs, delayed delivery (plugin), and per-message acknowledgment with automatic redelivery to another consumer on failure — genuine work-queue semantics.
- Competing consumers that just work: ten workers on one queue, each message to exactly one of them, backpressure via prefetch. This is the job-queue post made flesh.
- Native dead-lettering: rejected/expired messages route to a DLX automatically — the failure path is a broker feature, not application scaffolding.
Its constraints are the mirror image: a consumed message is gone (no replay, no "let the new analytics service re-read history"), ordering guarantees are per-queue-single-consumer at best, and while modern quorum queues + streams have raised throughput ceilings substantially, the log-structured firehose is not its home game.
Kafka: the memory specialist
Kafka's model: producers append to topics split into ordered partitions; the broker retains everything for a configured window (or forever, compacted); consumer groups track their own offsets. The broker doesn't push, doesn't route, doesn't forget — and from that austerity flow the superpowers:
- Replay. New consumer? Bug in yesterday's processing? Reset offsets and re-read a week of events. This single property is why Kafka anchors event-driven architectures at scale — the log is a source of truth, not a conveyor belt.
- Many independent readers. Billing, analytics, search-indexing and the fraud team all consume the same order stream at their own pace, at zero marginal cost to each other.
- Ordering per key. Partition by
order_idand every event for that order arrives in sequence to whichever consumer owns the partition — the property that makes stateful stream processing sane. - Throughput as a design consequence: sequential appends, zero-copy reads, horizontal partitions — millions of events/sec is a sizing exercise, not an achievement.
The bill: operational gravity (partitions, rebalances, retention math, ZooKeeper's ghost — KRaft helps, but this is a platform, not a service you casually run), no per-message ack/retry (a poison message blocks its partition until you build skip/DLQ logic), routing lives in consumers not the broker, and delayed/priority delivery are foreign concepts.
The decision, by workload shape
Task distribution (emails, PDFs, webhooks, image jobs)
→ RabbitMQ (or honestly: your framework's Redis/DB queue — see below)
Cross-service integration events, audit trails, "new consumers will appear"
→ Kafka
Event sourcing / stream processing / CDC firehose (Debezium etc.)
→ Kafka, no contest
Complex routing topologies, per-message TTL/priority/delay
→ RabbitMQ, no contest
Both shapes in one company (common!)
→ Both is a legitimate answer: Rabbit for work, Kafka for facts.
One broker doing both jobs badly is not the simplification it looks like.
The option the rivalry framing hides
Most applications reaching for either actually need neither yet. Laravel's Redis/database queues with Horizon, or .NET with Hangfire, deliver work-queue semantics with an ops footprint of approximately zero — and Redis Streams covers lightweight event-log needs into the hundreds of thousands of events. My graduation criteria, honestly stated: adopt RabbitMQ when you need cross-language consumers, routing topologies, or broker-level DLQ/priority semantics your framework queue fakes badly. Adopt Kafka when replayability and multiple independent consumers of the same stream become real requirements — usually around the same time multiple teams want the same facts. Adopting either because the architecture diagram looked lonely is how platform teams are born prematurely.
And whichever broker wins: the disciplines from the adjacent posts — idempotent consumers, designed retries, owned DLQs, lag on a dashboard — matter more than the logo. Brokers move messages; correctness was always your job.
Sizing this decision for a real system — or running the wrong one and feeling it? Messaging architecture reviews are a specialty; bring your topology diagram and your worst incident.