Kafka vs RabbitMQ — choosing a message broker
The most common messaging interview question. The wrong answer is “Kafka is faster.” The right answer starts from the model: RabbitMQ is a smart broker with dumb consumers (broker routes, tracks, and deletes messages); Kafka is a dumb broker with smart consumers (broker is an append-only log, consumers track their own position).
Deep dives: What is RabbitMQ, What is Kafka.
The core difference: queue vs log
RabbitMQ (queue): Kafka (log):
producer → exchange → queue producer → topic partition
consumer ACKs → message DELETED consumer reads at offset → log UNCHANGED
another consumer group re-reads from 0- RabbitMQ: a message is consumed destructively. Once acked, it’s gone. Competing consumers on one queue split the work.
- Kafka: messages are retained for a configured time/size regardless of consumption. Consumers are just cursors (offsets) over the log. New consumers can replay history.
Everything else follows from this.
Side by side
| Dimension | RabbitMQ | Kafka |
|---|---|---|
| Model | message queue (AMQP) | distributed, partitioned log |
| Consumption | destructive, per-message ack | offset-based, non-destructive |
| Replay | no (message deleted on ack) | yes — rewind offset, add new group |
| Routing | rich: direct/topic/fanout/headers exchanges | none in broker — topic + partition key only |
| Ordering | per-queue (breaks with multiple consumers/requeues) | strict per-partition |
| Delivery tracking | broker tracks each message’s state | broker tracks nothing; consumers commit offsets |
| Per-message features | TTL, priorities, delayed delivery, DLX | none (build in consumer or with retry topics) |
| Retention | until consumed (or TTL) | time/size-based (or compacted), days–forever |
| Fan-out to N consumers | bind N queues to one exchange | N consumer groups read same log — free |
| Throughput ceiling | tens of thousands msg/s per node (typical) | millions msg/s per cluster (sequential disk I/O, batching, zero-copy) |
| Latency | very low (sub-ms possible), push-based | low but batch-oriented, pull-based |
| Consumer scaling | add consumers to a queue, instant | bounded by partition count; triggers rebalance |
| Protocol | AMQP 0-9-1 (+ MQTT, STOMP) | custom binary protocol |
| Typical Python client | pika, aio-pika |
confluent-kafka, aiokafka |
When RabbitMQ fits
- Task distribution / background jobs — work queues with fair dispatch, retries, DLX. This is why Celery uses it (What is Celery).
- Complex routing — route by routing-key patterns or headers without consumer-side filtering (RabbitMQ exchanges and routing).
- Per-message control — priorities, per-message TTL, delayed retries, dead-lettering (RabbitMQ Exchanges, Queues, DLX, and Quorum Queues).
- RPC over messaging — request/reply with
reply_to+correlation_id. - Low-volume, low-latency command passing between services where each message is an instruction to do work once.
When Kafka fits
- Event streaming / EDA backbone — services publish facts, many independent consumers react (Event-Driven Architecture).
- Replay & audit — rebuild a read model, backfill a new service, reprocess after a bug fix. The killer feature queues can’t offer.
- High throughput — clickstreams, metrics, logs, CDC feeds.
- Stream processing — windowing, joins, aggregations over the log (Stream Processing — Windowing, Watermarks, State).
- Event sourcing / outbox delivery — the log is the source of truth (Transactional Outbox Pattern).
Rule of thumb: commands → RabbitMQ, events → Kafka. A command (“charge this card”) should be executed once by one worker and disappear. An event (“order placed”) is a fact that many consumers, present and future, may care about.
The difference is visible in four lines of consumer code. RabbitMQ — you tell the broker the message is done, and it stops existing:
async for message in queue:
async with message.process(): # ack on success
await handle(message.body) # requeue on exceptionKafka — you tell yourself where you got to, and the message stays:
async for msg in consumer:
await handle(msg.value)
# moves *my* offset only
await consumer.commit()commit() advances one consumer group’s cursor. Every other group, including one
created next year, still sees the message. That is the replay property, and it is
not a feature RabbitMQ is missing — it is a different data structure.
The rest of the field
| Broker | One-liner | Reach for it when |
|---|---|---|
| SQS / SNS | managed queue / pub-sub on AWS | you’re on AWS and want zero ops; no replay, 14-day max retention |
| Redis Streams | log-like structure inside Redis | you already run Redis and need lightweight streaming; not a durability story |
| NATS (JetStream) | lightweight cloud-native messaging | ultra-low latency service mesh–style messaging, simpler ops than Kafka (NATS and JetStream) |
| Pulsar | Kafka competitor, segmented storage | multi-tenancy, tiered storage, queue+stream in one system |
| IBM MQ | enterprise legacy standard | you didn’t choose it; the bank did (IBM MQ — Overview for Python Backends) |
Note Celery’s broker support: RabbitMQ and Redis are first-class; Kafka is not supported. If the team is Celery-based, “just use Kafka for tasks” is not a drop-in swap.
Using both is normal
A common production shape:
- RabbitMQ (or SQS) for work queues: emails, PDF generation, payment execution.
- Kafka for the event backbone: order/user/payment events feeding search indexing, analytics, notifications, and the data warehouse.
The saga/outbox machinery (Data Consistency Across Services) works over either; choose per stream of data, not per company.
Common interview confusions
- “Kafka is a message queue.” A consumer group makes it behave queue-like (each message processed by one member), but there’s no per-message ack/delete, no broker-side retry, no DLQ primitive — you build those with retry topics and consumer logic (Kafka delivery semantics — at-most-once, at-least-once, exactly-once).
- “RabbitMQ doesn’t scale.” It scales to very high volumes with clustering and quorum queues; it just doesn’t scale the same way (no partition-parallel consumption model, ordering vs parallelism trade-offs hit earlier).
- “Kafka guarantees ordering.” Only per partition. Order across a topic requires one partition (killing parallelism) or a partition key that groups what must be ordered (e.g.,
order_id). - “Exactly-once means I can forget idempotency.” No — see Kafka delivery semantics — at-most-once, at-least-once, exactly-once; end-to-end you still design idempotent consumers (Celery Task Idempotency).
Interview angle 4
- “Kafka vs RabbitMQ — when would you pick each?” — Lead with queue-vs-log, then: destructive consumption vs replay, broker routing vs partition keys, commands vs events. Give one concrete example of each.
- “How would you add a new consumer of order events a year after launch?” — Kafka: new consumer group, replay from offset 0 (if retention allows). RabbitMQ: you can’t — messages are gone; you’d need to re-emit or backfill from the DB.
- “Can you get strict global ordering in Kafka?” — One partition only; explain the throughput cost and the partition-key compromise.
- “Your team uses Celery — where does Kafka fit?” — Not as the Celery broker; as the event backbone next to it.