DEV Community

Cover image for RabbitMQ vs Kafka for Data Engineering: Queues vs Logs, When Each Wins
Gowtham Potureddi
Gowtham Potureddi

Posted on

RabbitMQ vs Kafka for Data Engineering: Queues vs Logs, When Each Wins

rabbitmq vs kafka is the question that never dies in a data engineering interview, and it is almost always asked the wrong way — as if one tool were a faster version of the other. They are not the same shape at all. RabbitMQ is a message queue: a smart broker that routes each message to a queue, hands it to a consumer, and deletes it once the consumer acknowledges. Kafka is a distributed log: a dumb broker that appends every message to an ordered file and lets each consumer track its own position, so the same message can be read again tomorrow by a reader that did not exist today.

That single distinction — the broker owns delivery and forgets the message versus the broker owns nothing but the log and remembers everything — is the root from which every other difference grows: how ordering works, whether you can replay, how you scale consumers, what "at-least-once" means, and which throughput ceiling you hit. This guide walks the four things an interviewer actually probes — RabbitMQ's exchange-and-ack model, Kafka's partitioned-log-and-offset model, the delivery-and-ordering trade-offs, and the competing-consumers-versus-consumer-groups scaling question — and pairs each with a Solution-Tail interview answer: code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.

PipeCode blog header for RabbitMQ vs Kafka — bold white headline 'Queues vs Logs' with subtitle 'RabbitMQ vs Kafka: delivery, ordering, replay' and a stylised split scene of a smart-broker exchange fanning to queues on the left and a partitioned append-only log with offsets on the right, on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution.

When you want hands-on reps immediately after reading, drill the queue practice library →, rehearse the log-and-offset patterns on the streaming practice set →, and harden your consumer logic on the event-processing practice set →.


On this page


1. Why the queue-vs-log split decides everything

A queue is a smart broker that forgets; a log is a dumb broker that remembers — that one axis explains every other difference

The one-sentence invariant: RabbitMQ moves a message and deletes it once someone accepts responsibility; Kafka records a message and keeps it until retention expires, no matter how many readers have seen it. Everything an interviewer will ask you to compare — routing, ordering, replay, throughput, delivery guarantees — is a consequence of that one design axis. Get this framing crisp in the first sentence and the rest of the answer writes itself.

Where the intelligence lives.

  • RabbitMQ — smart broker, dumb consumer. The broker does the hard work: it evaluates routing rules, decides which queue each message lands in, pushes messages to consumers, tracks which are outstanding, and re-delivers the ones nobody acknowledged. A consumer just receives and acks.
  • Kafka — dumb broker, smart consumer. The broker does almost nothing per message: it appends bytes to a partition and serves a byte range when asked. The consumer decides what to read, remembers its own position (the offset), and is responsible for not losing its place.

What happens to a message after it is consumed.

  • Queue — consumption is destructive. Once a RabbitMQ consumer acks a message, the broker removes it from the queue. There is exactly one logical copy in a queue, and after it is delivered-and-acked it is gone. This is why a queue is a work distributor.
  • Log — consumption is non-destructive. A Kafka read never removes anything. The record stays in the partition until a retention policy (time, size, or compaction) evicts it. Reading is just advancing a pointer, so a second consumer group can start from offset 0 and re-read the entire history.

What each shape is natively good at.

  • RabbitMQ is a task/command router. Rich routing (direct, topic, fanout), per-message TTL, priority queues, and request/reply RPC make it ideal for "do this unit of work exactly once, then forget it."
  • Kafka is an event log. High-throughput ingest, long retention, and replay make it ideal for "record what happened, and let many independent systems read the history at their own pace."

What interviewers listen for.

  • Do you open with "a queue deletes on ack, a log keeps on read" rather than "Kafka is faster"? — senior signal.
  • Do you say "smart broker vs smart consumer" to explain who tracks progress? — required framing.
  • Do you tie replay, fan-out, and ordering back to the log/queue distinction instead of listing them as unrelated features? — the whole point.
  • Do you avoid the trap of calling Kafka "just a better RabbitMQ"? — they solve different problems.

Worked example — the same event, two mental models

Detailed explanation. The fastest way to feel the difference is to trace one event, order_created, through both systems and watch what the broker does. In RabbitMQ the broker routes the event to a queue and, once the billing service acks it, the event is gone. In Kafka the broker appends the event at some offset, and it stays there for the whole retention window while billing, analytics, and a future fraud service each read it independently.

Question. One order_created event must reach a billing service today and a not-yet-built fraud service next quarter. What does each broker do with the event, and which design lets the fraud service see historical orders?

Input.

system broker action on order_created after billing consumes
RabbitMQ route to q.billing, push to consumer deleted on ack
Kafka append to orders partition at offset N still at offset N

Code.

RabbitMQ:  publish -> exchange -> q.billing -> billing acks -> message removed
Kafka:     produce -> orders[P0] @ offset N -> billing commits offset -> record stays
Enter fullscreen mode Exit fullscreen mode

Step-by-step explanation. In RabbitMQ the exchange routes order_created into q.billing; the billing consumer processes it and sends basic.ack, and the broker deletes it. There is no copy left for a future service — you would have had to bind a second queue before the event was published. In Kafka the record is appended once and billing merely advances its committed offset; the bytes remain, so next quarter the fraud service joins as a new consumer group, starts at offset 0, and replays every historical order.

Output.

capability RabbitMQ (queue) Kafka (log)
new reader sees old events only if its queue was bound first yes, seek to offset 0
message after ack/commit deleted retained until policy evicts
who tracks progress the broker the consumer group

Rule of thumb. If a future, unknown consumer must read past events, you want a log; if each message is a one-shot task to be done and forgotten, you want a queue.


2. RabbitMQ — exchanges, queues, bindings & acks

The smart broker: publishers never name a queue — they publish to an exchange, and bindings do the routing

RabbitMQ implements AMQP 0-9-1, and its whole model is that a publisher talks to an exchange, never to a queue. The exchange applies binding rules to route the message to zero or more queues; consumers subscribe to queues and acknowledge each message. Learn the four moving parts — exchange, binding, queue, ack — and RabbitMQ stops being mysterious.

The routing layer — exchanges and bindings.

  • Direct exchange. Routes a message to queues whose binding key exactly equals the message's routing_key. Use it for "send this to the payments worker."
  • Topic exchange. Routes by pattern on a dotted routing_key, where * matches one word and # matches zero or more. A binding order.*.eu catches order.created.eu but not order.created.us. This is the workhorse for selective subscription.
  • Fanout exchange. Ignores the routing key and broadcasts to every bound queue — the native pub/sub / fan-out primitive.
  • Headers exchange. Routes on message header attributes instead of the routing key, for the rare case where routing depends on structured metadata.

The reliability layer — acknowledgements.

  • Manual ack (basic.ack). The consumer tells the broker "I have safely processed this," and only then is the message removed. This is the default you want in production.
  • basic.nack / basic.reject. Signal failure; with requeue=true the broker puts the message back for another attempt, with requeue=false it is dropped or dead-lettered.
  • Publisher confirms. The broker asynchronously tells the publisher a message was safely persisted/routed, closing the gap on the produce side so you know the broker actually took ownership.
  • Auto-ack is a footgun. With auto-ack the message is considered delivered the instant it leaves the broker; a consumer crash mid-processing loses it silently. Avoid it unless loss is acceptable.

The control layer — fairness, TTL, and dead-letters.

  • Prefetch (basic.qos(prefetch_count=N)). Caps how many unacked messages a consumer may hold. prefetch_count=1 gives fair dispatch — a slow worker is not flooded while a fast one starves.
  • Dead-letter exchange (DLX). Messages that are rejected without requeue, exceed a TTL, or overflow a max-length queue are routed to a DLX, so failures land in a parking queue instead of vanishing.
  • Quorum queues. Raft-replicated queues for high availability; the modern default over classic mirrored queues when you need the queue to survive a broker failure.

Iconographic RabbitMQ diagram — a publisher sending to an exchange that routes by binding to three queues (direct, topic, fanout), competing consumers acking messages off one queue, and a dead-letter exchange receiving a rejected message.

Worked example — a topic exchange routing to two queues

Detailed explanation. The canonical RabbitMQ example that proves you understand routing is a topic exchange with two queues bound by different patterns. One queue collects everything, another collects only EU orders. The publisher never knows the queues exist; the bindings decide everything.

Question. Publish order.created.eu and order.created.us to a topic exchange where q.audit is bound with order.# and q.eu is bound with order.*.eu. Which queue gets which message?

Input.

routing_key q.audit binding order.# q.eu binding order.*.eu
order.created.eu match match
order.created.us match no match

Code.

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
ch = conn.channel()

ch.exchange_declare(exchange="orders", exchange_type="topic", durable=True)
ch.queue_declare(queue="q.audit", durable=True)
ch.queue_declare(queue="q.eu", durable=True)
ch.queue_bind(exchange="orders", queue="q.audit", routing_key="order.#")
ch.queue_bind(exchange="orders", queue="q.eu", routing_key="order.*.eu")

for rk in ("order.created.eu", "order.created.us"):
    ch.basic_publish(
        exchange="orders",
        routing_key=rk,
        body=rk.encode(),
        properties=pika.BasicProperties(delivery_mode=2),  # persistent
    )
conn.close()
Enter fullscreen mode Exit fullscreen mode

Step-by-step explanation. exchange_declare(type="topic") creates a pattern-routing exchange. q.audit binds with order.# where # matches any number of trailing words, so it catches both keys. q.eu binds with order.*.eu, where * matches exactly one word and the third word must be eu, so only order.created.eu matches. When the publisher sends the two keys, the broker evaluates each binding independently and copies the message into every matching queue — the EU order lands in both queues, the US order lands only in the audit queue.

Output.

message delivered to
order.created.eu q.audit, q.eu
order.created.us q.audit

Rule of thumb. In RabbitMQ, routing is a broker concern configured with bindings — the publisher stays ignorant of consumers, which is exactly why selective subscription is trivial.

RabbitMQ interview question on fair dispatch and safe acks

Question. You run three worker consumers on one q.jobs queue. Jobs take wildly different times, and if a worker crashes mid-job the job must not be lost. How do you configure RabbitMQ so work is dispatched fairly and no in-flight job is ever dropped?

Solution Using prefetch=1 with manual acknowledgements

Code.

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
ch = conn.channel()
ch.queue_declare(queue="q.jobs", durable=True)

ch.basic_qos(prefetch_count=1)          # fair dispatch: one unacked job at a time

def handle(chan, method, props, body):
    try:
        do_work(body)                   # may take seconds to minutes
        chan.basic_ack(method.delivery_tag)             # remove only after success
    except Exception:
        chan.basic_nack(method.delivery_tag, requeue=True)   # crash-safe redelivery

ch.basic_consume(queue="q.jobs", on_message_callback=handle, auto_ack=False)
ch.start_consuming()
Enter fullscreen mode Exit fullscreen mode

Step-by-step trace.

event worker A (fast) worker B (slow) broker action
dispatch job1, job2 job1 (unacked) job2 (unacked) each holds 1, none prefetched ahead
A acks job1 free still on job2 broker sends job3 to A, not B
B crashes on job2 connection drops job2 unacked -> requeued for A
A acks job2, job3 free jobs removed only after ack
  1. basic_qos(prefetch_count=1) tells the broker never to hand a consumer a second message until the first is acked, so a slow worker cannot hoard a backlog while a fast worker idles.
  2. auto_ack=False plus basic_ack after do_work means the message is deleted only on success — the broker keeps it as "unacked, in-flight" until then.
  3. If worker B's connection drops before it acks, the broker marks job2 unacknowledged and redelivers it to another consumer, so no in-flight job is lost.
  4. On a genuine processing error, basic_nack(requeue=True) explicitly returns the job for retry (or route to a DLX with requeue=False after N attempts).

Output:

property result
dispatch fairness each worker holds at most one unacked job
crash safety unacked jobs are redelivered, never dropped
duplicates possible? yes — redelivery means make do_work idempotent

Why this works — concept by concept:

  • Prefetch (QoS) — capping unacked messages per consumer turns round-robin push into fair dispatch, matching work to capacity instead of blindly balancing counts.
  • Manual ack — deferring the ack until after successful processing makes the queue the source of truth for "not yet done," so a crash simply leaves the job outstanding.
  • Redelivery — the broker's re-queue of unacked messages is what gives at-least-once delivery; the flip side is that the same job can run twice.
  • Idempotent handler — because redelivery creates duplicates, correctness depends on do_work being safe to run twice, not on the broker.
  • Cost — fairness and safety cost one network round-trip per ack and a bounded in-flight window of O(prefetch × consumers).

Queue
Topic — queue
Work-queue and fair-dispatch problems

Practice →

Event processing Topic — event-processing Ack, retry and dead-letter handling problems

Practice →


3. Kafka — partitioned log, offsets & consumer groups

The dumb broker: a topic is a set of append-only partitions, and the consumer — not the broker — remembers where it is

Kafka inverts RabbitMQ's model. A topic is split into partitions, and each partition is an ordered, immutable, append-only log. The broker's only jobs are to append records and to serve a byte range starting at a requested offset. It does not track who has read what; the consumer group does. This inversion is why Kafka scales to millions of messages a second and why replay is free.

The storage model — partitions and offsets.

  • Partition = ordered log. Within a partition, every record has a strictly increasing offset (0, 1, 2, …) and records are never mutated or reordered. Ordering is guaranteed only inside a single partition.
  • Offset = the consumer's bookmark. An offset is just an integer position. "Consuming" means reading forward from your current offset; committing an offset means recording how far you got, in the internal __consumer_offsets topic.
  • Partition count = the parallelism ceiling. A partition is the unit of parallelism; you cannot have more actively-consuming members of a group than there are partitions.

The producer side — how a record picks a partition.

  • Keyed records. If a record has a key, Kafka hashes it (hash(key) % num_partitions) so all records with the same key land in the same partition — and therefore stay ordered relative to each other. Keying by customer_id keeps a customer's events in order.
  • Keyless records. With no key, the producer spreads records across partitions (sticky batching) for throughput, sacrificing cross-record order.
  • Acks and idempotence. acks=all waits for in-sync replicas; enable.idempotence=true makes the producer safe against its own retries so a network hiccup does not duplicate a record.

The consumer side — groups and retention.

  • Consumer group = cooperative readers. Kafka assigns each partition to exactly one consumer within a group, so N partitions can be processed by up to N consumers in parallel. Adding a consumer beyond the partition count leaves it idle.
  • Independent groups = fan-out. Every group has its own offsets, so billing and analytics groups read the same topic without affecting each other — native multi-subscriber fan-out with no extra queues.
  • Retention keeps data after read. retention.ms / retention.bytes evict old segments by time or size; log compaction instead keeps the latest record per key forever. Because eviction is policy-driven, not consumption-driven, the data survives being read — which is what makes replay possible.

Iconographic Kafka diagram — a topic split into partitions as append-only numbered cells, a producer keying records into a partition, two consumer groups reading the same log at independent offsets, and a retention window keeping data after read.

Worked example — keyed produce keeps a customer's events ordered

Detailed explanation. The single most important Kafka behaviour to demonstrate is that keying controls partitioning, and partitioning controls ordering. Send three events for one customer and two for another; the shared key routes each customer's events to one partition, preserving their order, while different customers spread across partitions for throughput.

Question. Produce events keyed by customer_id to a 3-partition topic. Show which partition each customer's events land in and what ordering guarantee you get.

Input.

event key (customer_id) partition = hash(key) % 3
e1, e2, e3 c-42 1
e4, e5 c-99 2

Code.

from confluent_kafka import Producer

p = Producer({"bootstrap.servers": "localhost:9092", "enable.idempotence": True})

events = [
    ("c-42", "login"), ("c-42", "add_to_cart"), ("c-42", "checkout"),
    ("c-99", "login"), ("c-99", "logout"),
]

for key, value in events:
    p.produce(topic="events", key=key, value=value)  # key -> partition by hash
p.flush()
Enter fullscreen mode Exit fullscreen mode

Step-by-step explanation. Each produce call passes a key; Kafka's default partitioner computes hash(key) % num_partitions, so every c-42 event maps to the same partition (say P1) and every c-99 event to another (P2). Within P1 the three c-42 events are appended in call order at consecutive offsets, so login → add_to_cart → checkout is preserved. enable.idempotence=True ensures a produce retry after a transient error does not append a duplicate. The two customers occupy different partitions, so they are processed in parallel with no ordering relationship between them.

Output.

partition records in offset order ordering guarantee
P1 login, add_to_cart, checkout (c-42) ordered for c-42
P2 login, logout (c-99) ordered for c-99

Rule of thumb. Order in Kafka is a per-partition property — choose the partition key to match the entity whose order you must preserve, and never assume order across partitions.

Kafka interview question on replay and offset management

Question. Analytics loaded a week of data with a bug in the transform. The raw events are still in the events topic (7-day retention). How do you reprocess the last three days without touching the producer or the billing consumer, and why is this possible in Kafka but not RabbitMQ?

Solution Using a new consumer group seeking by timestamp

Code.

from confluent_kafka import Consumer, TopicPartition
import datetime as dt

c = Consumer({
    "bootstrap.servers": "localhost:9092",
    "group.id": "analytics-reprocess-v2",   # brand-new group = independent offsets
    "enable.auto.commit": False,
})

topic = "events"
three_days_ago = int((dt.datetime.utcnow() - dt.timedelta(days=3)).timestamp() * 1000)

parts = [TopicPartition(topic, pn, three_days_ago) for pn in (0, 1, 2)]
for tp in c.offsets_for_times(parts):       # first offset at-or-after the timestamp
    c.assign([tp])                          # start reading from that offset
    while True:
        msg = c.poll(1.0)
        if msg is None:
            break
        reprocess(msg)                      # corrected transform
Enter fullscreen mode Exit fullscreen mode

Step-by-step trace.

step action effect
1 new group.id=analytics-reprocess-v2 fresh offsets, isolated from billing + old analytics
2 offsets_for_times(3 days ago) resolves the offset boundary per partition
3 assign + poll from that offset rereads the last 3 days of retained records
4 billing group untouched its committed offsets never move
  1. A new consumer group has its own entry in __consumer_offsets, so it starts fresh without disturbing any existing reader's position.
  2. offsets_for_times maps a wall-clock timestamp to the first offset at-or-after it in each partition, giving a precise replay boundary of "3 days ago."
  3. Because retention is 7 days, the records for the last 3 days physically still exist on disk — reading them is just serving old byte ranges.
  4. The billing consumer group reads the same topic with its own offsets and is completely unaffected; nothing is deleted by the replay.

Output:

reader offsets moved data reprocessed
analytics-reprocess-v2 3 days ago → now yes, corrected transform
billing (existing group) unchanged none — isolated

Why this works — concept by concept:

  • Retention over consumption — data lives until a time/size policy evicts it, not until it is read, so the raw events are still on disk to reprocess.
  • Offsets are per-group — a new group.id gets independent bookmarks, letting you replay without moving any production consumer's position.
  • Seek by timestampoffsets_for_times turns "the last three days" into exact per-partition offsets, so replay is deterministic and bounded.
  • Non-destructive reads — because reading never removes records, RabbitMQ cannot do this: an acked message is already gone from the queue.
  • Cost — replay is O(records in the window) of sequential disk reads, paid by the new group alone; producers and other consumers are untouched.

Streaming
Topic — streaming
Partitioned-log and offset problems

Practice →

Event processing Topic — event-processing Replay and reprocessing problems

Practice →


4. Delivery semantics, ordering & throughput

At-least-once is the honest default on both — the difference is where ordering is scoped and how much raw throughput you can buy

The three properties interviewers stress-test — delivery guarantee, ordering, and throughput — are where the queue-vs-log split shows up as concrete engineering constraints. The headline: both systems default to at-least-once, so duplicates are your problem either way; ordering is per-queue in RabbitMQ and per-partition in Kafka; and Kafka buys far higher throughput by giving up per-message routing.

Delivery semantics — what "once" really means.

  • At-most-once. Deliver and forget; a crash loses the message. RabbitMQ auto-ack and Kafka "commit offset before processing" both land here. Fast, lossy, rarely what you want.
  • At-least-once (the default). Redeliver until acknowledged/committed; a crash after processing but before ack causes a duplicate. RabbitMQ manual-ack-after-work and Kafka commit-offset-after-process both give this.
  • Exactly-once. RabbitMQ has no native exactly-once — you get at-least-once plus consumer-side dedup. Kafka offers exactly-once within Kafka via the idempotent producer plus transactions (transactional.id), covering read-process-write inside the cluster; end-to-end into an external sink still needs an idempotent write.

Ordering — the scope is the whole game.

  • RabbitMQ. A single queue with a single consumer preserves publish order. Add competing consumers and order across messages is no longer guaranteed; a requeued message can also jump behind newer ones. For ordered-by-key delivery you use a consistent-hash exchange to pin a key to one queue.
  • Kafka. Order is guaranteed within a partition and nowhere else. Same-key records share a partition and stay ordered; that is the entire ordering contract. More partitions means more parallelism but no global order.

Throughput — why the numbers differ so much.

  • Kafka is built for volume. Sequential appends to a partition file, large producer batches, consumer pull with configurable fetch sizes, and zero-copy transfer to the network let a single cluster sustain very high sustained throughput. The consumer pulls, so back-pressure is natural.
  • RabbitMQ optimizes for routing and low latency. Per-message routing, acks, and (often) persistence add per-message overhead, so a single queue tops out far below a Kafka partition — but for moderate volumes with complex routing and low tail latency it is excellent, and it pushes to consumers for immediate delivery.

Iconographic diagram comparing delivery semantics, ordering, and throughput — at-most-once / at-least-once / exactly-once notches, RabbitMQ single-consumer ordering vs Kafka per-partition ordering, and a throughput meter contrasting per-message routing with sequential batched log writes.

Worked example — where a duplicate sneaks in under at-least-once

Detailed explanation. The classic interview trap is assuming "the broker acked, so it happened once." Under at-least-once, a crash in the window between doing the work and recording the ack/commit replays the message. Tracing that window on both systems shows the duplicate is inherent, not a bug.

Question. A consumer processes a payment, then crashes before it can ack (RabbitMQ) or commit the offset (Kafka). What does each broker do on restart, and how many times is the payment applied?

Input.

moment RabbitMQ state Kafka state
payment applied message still unacked offset not yet committed
consumer crashes broker holds it in-flight offset stays at last commit
consumer restarts broker redelivers poll returns from last commit

Code.

process_payment(msg)      # at-least-once: side effect lands BEFORE the ack/commit
channel.basic_ack(tag)    # RabbitMQ: crash before this line -> redelivery
consumer.commit(msg)      # Kafka: crash before this line -> re-poll same record
Enter fullscreen mode Exit fullscreen mode

Step-by-step explanation. The side effect (process_payment) lands first; the ack/commit is a separate step. If the process dies in between, neither broker knows the work was done. RabbitMQ still holds the message as unacknowledged and redelivers it; Kafka's committed offset never advanced, so the next poll returns the same record. Either way the payment is applied a second time — unless the write itself is idempotent (e.g. an upsert keyed by payment_id).

Output.

guarantee crash-in-window result fix
at-least-once (both) payment applied twice idempotent write keyed by payment_id
at-most-once payment possibly lost usually unacceptable for money
exactly-once (Kafka, in-cluster) one effect idempotent producer + transaction

Rule of thumb. Design every consumer to be idempotent; at-least-once means the broker guarantees "no loss," never "no duplicate," and the dedup key is your responsibility.

RabbitMQ vs Kafka interview question on ordered processing at scale

Question. You must process events for each account_id strictly in order, but you also need to parallelize across accounts for throughput. Show how Kafka gives you both, and what the RabbitMQ equivalent requires.

Solution Using key-based partitioning for per-key order with parallelism

Code.

from confluent_kafka import Producer, Consumer

producer = Producer({"bootstrap.servers": "localhost:9092",
                     "enable.idempotence": True})
producer.produce("account-events", key=str(account_id),   # same key -> same partition
                 value=payload)                            # -> per-account order held

consumer = Consumer({"bootstrap.servers": "localhost:9092",
                     "group.id": "account-processor",      # each partition -> ONE member
                     "enable.auto.commit": False})
consumer.subscribe(["account-events"])                     # a member reads a key in order
rabbitmq_equivalent = (
    "consistent-hash exchange pins account_id -> one queue, "
    "with exactly one consumer per queue to preserve order"
)
Enter fullscreen mode Exit fullscreen mode

Step-by-step trace.

account_id key-hash → partition consumer (group member) order held?
a-1 P0 member 1 yes (single reader of P0)
a-2 P1 member 2 yes (single reader of P1)
a-1 again P0 member 1 yes — same partition, appended after
  1. Producing with key=account_id sends every event for an account to the same partition, so their offsets are strictly increasing and order is preserved.
  2. Kafka assigns each partition to exactly one consumer in the group, so a single member processes all of an account's events in offset order — no cross-consumer reordering.
  3. Different accounts hash to different partitions and are processed by different members concurrently, giving throughput without breaking per-account order.
  4. The RabbitMQ route to the same guarantee is a consistent-hash exchange that pins each account_id to one queue, with a single consumer per queue — you must give up competing consumers on that queue to keep order.

Output:

requirement Kafka mechanism RabbitMQ mechanism
per-key order same-key → same partition consistent-hash exchange → one queue
cross-key parallelism more partitions + group members more queues, one consumer each
trade-off parallelism ≤ partitions lose fan-out of competing consumers per key

Why this works — concept by concept:

  • Key → partition mapping — hashing the key to a partition is what converts "ordered" from a global (expensive) property into a per-key (cheap, parallel) one.
  • One partition, one consumer — the group protocol's single-owner rule is precisely what prevents two threads from reordering a key's events.
  • Parallelism ceiling — throughput scales with partition count, so you size partitions for peak concurrency up front (repartitioning later is disruptive).
  • RabbitMQ consistent-hash — RabbitMQ reaches the same guarantee only by pinning a key to a single queue+consumer, trading away the work-sharing that competing consumers normally give.
  • Cost — per-key order costs one hash per record and caps concurrency at the partition (or queue) count; it is far cheaper than any global-ordering scheme.

Event processing
Topic — event-processing
Delivery-semantics and idempotency problems

Practice →

Queue Topic — queue Ordered-processing and consistent-hash problems

Practice →


5. Competing consumers vs consumer groups — when each wins

Both scale out by adding readers — but one shares a queue and forgets, the other splits partitions and remembers

The last thing interviewers push on is scaling and selection: how you add throughput, and how you choose. RabbitMQ scales with the competing-consumers pattern; Kafka scales with consumer groups. They look similar — several readers dividing the load — but the mechanics and the trade-offs are opposite, and the decision framework falls straight out of the queue-vs-log split.

How each scales out.

  • RabbitMQ competing consumers. Attach more consumers to one queue and the broker round-robins (with prefetch) across them. There is no partition concept, so you can add a 20th consumer to a single queue freely — the only cost is that ordering across the queue is gone. Perfect for a pool of interchangeable task workers.
  • Kafka consumer groups. Add members to a group and Kafka reassigns partitions among them, but active parallelism is capped at the partition count. A 4-partition topic can usefully feed at most 4 consumers per group; the fifth sits idle. You scale by choosing enough partitions up front.

Replay and multiple independent readers.

  • RabbitMQ. One logical copy per queue, deleted on ack. To fan the same message to several subsystems you bind several queues to a fanout exchange before publishing; there is no going back to re-read what a queue already dropped.
  • Kafka. Every consumer group reads the full log independently, and any group can rewind by resetting its offsets. Fan-out and replay are built in — add a group, seek to an offset, done.

The decision framework.

  • Reach for RabbitMQ when you need rich routing (topic or headers exchanges), request/reply RPC, per-message TTL or priority, a task/work queue where each message is done once and discarded, and low, predictable latency. It is a superb job dispatcher and command bus.
  • Reach for Kafka when you need high-throughput event streaming, durable retention as a system of record, replay/backfill, multiple independent consumers of the same stream, ordering by key at scale, or a backbone that stream processors (Flink, Kafka Streams) tap. It is an event log and integration spine.
  • It is not either/or. Many platforms run both: Kafka as the durable event backbone and RabbitMQ for low-latency command routing and RPC. Choose per workload shape, not by allegiance.

Iconographic decision diagram — RabbitMQ competing consumers sharing one queue (add consumers to scale, deletes on ack) beside Kafka consumer groups bound by partition count (reread by offset, replay), with a decision signpost pointing routing and tasks to RabbitMQ and streaming, replay, and fan-out to Kafka.

Worked example — adding a fifth reader to each system

Detailed explanation. The cleanest way to expose the scaling difference is to add readers past a limit and watch what happens. RabbitMQ happily uses a fifth consumer on a queue; Kafka leaves a fifth consumer idle on a 4-partition topic. Same intent, opposite outcome.

Question. A RabbitMQ queue and a 4-partition Kafka topic each have 4 readers keeping up with load. You add a 5th reader to each. What does each do with the extra reader?

Input.

system unit of parallelism readers before → after
RabbitMQ queue the queue (no partitions) 4 → 5
Kafka topic (4 partitions) the partition 4 → 5

Code.

RabbitMQ:  5 consumers on q.tasks  -> broker round-robins across all 5 (prefetch each)
Kafka:     5 members in group on a 4-partition topic
           -> 4 own one partition each, the 5th is assigned nothing (idle standby)
Enter fullscreen mode Exit fullscreen mode

Step-by-step explanation. RabbitMQ has no fixed number of "slots" on a queue; the broker simply hands the next ready message to whichever of the five consumers is free, so the fifth consumer immediately shares the load. Kafka assigns whole partitions, and a partition can belong to only one group member at a time; with four partitions and five members, the fifth has nothing to own and stays idle until the partition count grows or another member leaves. To use that fifth Kafka consumer you must increase partitions.

Output.

system 5th reader to scale further
RabbitMQ actively shares the queue keep adding consumers
Kafka idle (no free partition) increase partition count

Rule of thumb. RabbitMQ scales consumers freely and loses order; Kafka scales only up to the partition count and keeps per-partition order — size partitions for your peak concurrency ahead of time.

RabbitMQ vs Kafka interview question on choosing the backbone

Question. A payments platform needs (a) a durable, replayable record of every transaction that analytics, fraud, and ledger teams each read independently, and (b) a low-latency task queue that dispatches "send receipt email" jobs to a worker pool, retrying failures. Which system for which job, and why not one for both?

Solution Using Kafka for the event log and RabbitMQ for the task queue

Code.

(a) System of record  -> Kafka topic "transactions" (retention = 30d, keyed by account_id)
      consumer groups: analytics, fraud, ledger   # 3 independent offsets, all replayable
      -> add a team later? new group, seek offset 0, replay full history

(b) Receipt emails    -> RabbitMQ queue "q.emails" behind a direct exchange
      competing consumers: N email workers (prefetch=1, manual ack)
      failures -> nack(requeue=false) -> DLX "q.emails.dead" after max retries
Enter fullscreen mode Exit fullscreen mode

Step-by-step trace.

requirement chosen system mechanism
replayable transaction log Kafka retention + per-group offsets
3 independent readers Kafka 3 consumer groups, isolated
add a future reader Kafka new group seeks offset 0
dispatch email jobs RabbitMQ competing consumers on q.emails
retry + park failures RabbitMQ nack + dead-letter exchange
  1. The transaction stream is a system of record: it must be durable, ordered per account, and readable by several teams at their own pace — exactly the log shape, so Kafka.
  2. Independent consumer groups give analytics, fraud, and ledger their own offsets; a new team added next year replays history by starting a fresh group at offset 0.
  3. The email job is a transient command: do it once, retry on failure, then forget — exactly the queue shape, so RabbitMQ with competing consumers and a DLX.
  4. Forcing one tool to do both is worse: Kafka makes per-message TTL, priorities, and easy DLX retry awkward, while RabbitMQ cannot cheaply retain and replay a month of transactions for future readers.

Output:

job system why the other loses
transaction system of record Kafka RabbitMQ deletes on ack — no replay for new readers
receipt-email task queue RabbitMQ Kafka lacks per-message TTL/priority + simple redelivery

Why this works — concept by concept:

  • Log for records — a durable, replayable, multi-reader stream is the log's home turf; retention plus per-group offsets serve unknown future consumers for free.
  • Queue for tasks — a transient, retryable, done-once job is the queue's home turf; competing consumers plus a DLX give clean work distribution and failure parking.
  • Fan-out difference — Kafka fans out by adding groups (no republish); RabbitMQ fans out only to queues bound before publishing.
  • Right tool per shape — matching workload shape to broker shape beats forcing one broker to fake the other's strengths.
  • Cost — running both adds operational surface, but each workload runs on its cheapest, simplest path instead of fighting the wrong abstraction.

Streaming
Topic — streaming
Consumer-group and fan-out design problems

Practice →

Queue Topic — queue Competing-consumers and backbone-selection problems

Practice →


Cheat sheet — messaging recipes

RabbitMQ topic exchange — bind and consume.

ch.exchange_declare("orders", "topic", durable=True)
ch.queue_declare("q.eu", durable=True)
ch.queue_bind("q.eu", "orders", routing_key="order.*.eu")
ch.basic_consume("q.eu", on_message_callback=handle, auto_ack=False)
Enter fullscreen mode Exit fullscreen mode

RabbitMQ fair dispatch — prefetch + manual ack.

ch.basic_qos(prefetch_count=1)        # one unacked message per consumer
ch.basic_ack(method.delivery_tag)     # on success: remove only after processing
Enter fullscreen mode Exit fullscreen mode

RabbitMQ dead-letter queue.

ch.queue_declare("q.jobs", durable=True, arguments={
    "x-dead-letter-exchange": "dlx",  # rejected/expired -> DLX
    "x-message-ttl": 60000,           # 60s TTL
})
Enter fullscreen mode Exit fullscreen mode

Kafka keyed produce (per-key ordering).

producer.produce("events", key=str(account_id), value=payload)  # same key -> same partition
producer.flush()
Enter fullscreen mode Exit fullscreen mode

Kafka consumer group + manual offset commit.

c = Consumer({"bootstrap.servers": "...", "group.id": "billing",
              "enable.auto.commit": False})
c.subscribe(["events"])
msg = c.poll(1.0); process(msg); c.commit(msg)   # commit AFTER processing = at-least-once
Enter fullscreen mode Exit fullscreen mode

Kafka replay (new group, seek by offset).

c = Consumer({"group.id": "reprocess-v2", "enable.auto.commit": False, ...})
tp = TopicPartition("events", 0, 0)   # partition 0, offset 0
c.assign([tp])                        # reread the whole partition
Enter fullscreen mode Exit fullscreen mode

Queue-vs-log picker.

Need Choose
Rich routing / RPC / per-message TTL / priority RabbitMQ
Task/work queue, done-once-then-forget RabbitMQ
High-throughput event streaming Kafka
Replay / backfill / new readers of history Kafka
Multiple independent readers of one stream Kafka
Order by key at scale Kafka (partition by key)

Frequently asked questions

What is the difference between RabbitMQ and Kafka?

RabbitMQ is a message queue (a smart broker): it routes each message through an exchange to a queue, pushes it to a consumer, and deletes it once the consumer acknowledges. Kafka is a distributed log (a dumb broker): it appends every message to an ordered, partitioned log and lets each consumer group track its own offset, so messages are retained and can be reread. The practical upshot is that RabbitMQ excels at routing and task distribution, while Kafka excels at high-throughput streaming, retention, and replay.

Is Kafka a message queue?

Not in the traditional sense. A classic queue deletes a message once it is consumed, so only one logical copy exists; Kafka never deletes on consume — it keeps records until a retention or compaction policy evicts them, and any number of consumer groups can read the same records independently. Kafka can emulate a work queue (a single consumer group divides partitions among members), but its core model is an append-only log, not a destructive queue.

Which is faster, RabbitMQ or Kafka?

For raw sustained throughput Kafka is typically far ahead, because it writes sequentially to partition files, batches aggressively, and uses zero-copy transfer, so a cluster can handle very high message volumes. RabbitMQ adds per-message routing and acknowledgement overhead, so a single queue tops out lower — but it often delivers lower per-message latency for moderate volumes and gives you routing Kafka cannot. "Faster" depends on whether you are optimizing for bulk throughput (Kafka) or low-latency routed delivery at modest scale (RabbitMQ).

How does Kafka replay messages when RabbitMQ cannot?

Kafka retains records on disk according to a time, size, or compaction policy — independent of whether they have been read — and a consumer's position is just an offset it controls. To replay, you start a new consumer group or reset an existing group's offsets (to an earlier offset or a timestamp) and read the history again. RabbitMQ cannot do this because a message is removed from its queue once a consumer acks it; there is no retained copy to go back to, so any future reader must have had a queue bound before the message was published.

What delivery guarantees do RabbitMQ and Kafka provide?

Both default to at-least-once: messages are redelivered until acknowledged (RabbitMQ) or until the offset is committed after processing (Kafka), which means a crash in the processing-then-ack window produces duplicates. Both can be configured for at-most-once (ack/commit before processing) if occasional loss is acceptable. Kafka additionally offers exactly-once semantics within Kafka using the idempotent producer plus transactions; RabbitMQ has no native exactly-once, so you achieve effective once-only processing by making consumers idempotent.

When should I use RabbitMQ instead of Kafka?

Choose RabbitMQ when you need flexible routing (topic, headers, fanout), request/reply RPC, per-message TTL or priorities, or a straightforward task queue where each job is processed once and discarded with easy retries and dead-lettering — and when your volumes are moderate and low latency matters more than replay. Choose Kafka when you need a durable, replayable event log, very high throughput, ordering by key at scale, or multiple independent consumers reading the same stream. Many architectures run both, using Kafka as the event backbone and RabbitMQ for low-latency command dispatch.

Practice on PipeCode

Pipecode.ai is Leetcode for Data Engineering — every idea above, from RabbitMQ's prefetch-and-ack fair dispatch to Kafka's keyed partitioning, consumer groups, and offset replay, maps to a hands-on practice room where you build the consumer against real graded inputs. PipeCode pairs each reading with 450+ DE-focused problems and a real-time scoring engine, so your answer to "how would you make this consumer idempotent and replayable?" holds up under a senior interviewer's depth probes.

Practice queue problems now →
Streaming-design drills →

Top comments (0)