Skip to content
intermediate Phase 8 · Messaging and Event-Driven Architecture

Message Queues and Brokers

Buffer and deliver messages with brokers like Kafka, RabbitMQ, and SQS.

55m
0 problems
Topic Progress 0%

Absorbing a Flash Spike: How a Buffer Turns a 40x Burst Into a Backlog

A product launch hits at 09:00: 40,000 orders land in 90 seconds against a payment-verification service that can process 900 per second. In the synchronous design the API waits for verification, threads pile up, p99 goes from 120 ms to 11 seconds, the load balancer marks the pool unhealthy, and the whole site 503s — even though the capacity problem (900/s vs 666/s average) was mild. The queue's job is to change the shape of that failure: producers hand work to a broker and return immediately; consumers pull at their own sustainable rate, so the burst becomes visible backlog depth instead of cascading latency.

That reframing is the whole value proposition, and it has three concrete effects:

Without queue With queue
Peak arrival rate must be served instantly by consumers Consumers serve average rate; peak becomes backlog
Slow consumer stalls the caller Slow consumer affects only its own stream
Consumer outage fails the user's request Consumer outage delays work; producers still succeed
Scaling = scale producers and consumers together Scale consumers independently by partition/worker count

The cost of decoupling is that you now have two failures to manage instead of one: backlog growth and message loss. A queue does not make work disappear — if the sustained rate is below the sustained arrival rate, the backlog grows without bound and you eventually hit retention limits (Kafka: time/bytes retention deletes oldest messages; RabbitMQ/SQS: memory or queue-length limits push back on producers). The operational skill is watching consumer lag (oldest unprocessed message age) as the primary SLA signal: lag under a few seconds means the buffer is working; lag in minutes means you are borrowing time, and lag approaching retention means you are about to lose work.

Which broker for which shape — the comparison interviewers ask for:

RabbitMQ   smart broker, dumb consumers: routes + acks, per-queue semantics,
           per-message (not ordered-across-queue) delivery. Transient work,
           RPC-style task queues, priority, TTL/dead-letter exchanges.
Kafka      dumb broker, smart consumers: immutable ordered log per partition,
           offsets owned by consumers, retention-based replay. Event streams,
           high throughput, consumers that need history (analytics, CDC, search indexing).
SQS        fully managed simple queue: standard = at-least-once, unordered;
           FIFO = dedup + per-message-group ordering. Work queues with no ops burden.
NATS       lightweight pub/sub + core streams, low-latency fan-out.

Backpressure deserves its own rule: the queue must be the thing that says no, or nothing says no. If producers can publish infinitely fast while consumers crawl, you need either bounded queue length with producer-side throttling or a consumer autoscaling policy keyed on lag. Teams that skip this discover it during an incident: the disk fills, the broker starts refusing publishes, and now the ingress path is down because the processing path was slow — the exact coupling the queue was supposed to remove.

Two further design decisions frame everything downstream: pull vs push (pull lets consumers declare their own rate; push requires broker-side prefetch tuning and risks overwhelming a slow consumer) and delivery semantics (at-most-once loses work, at-least-once duplicates it, exactly-once requires idempotent effects or transactional sinks — covered in Event-Driven Architecture Patterns). An interview answer that says 'the queue decouples producers and consumers' and stops is incomplete; say what it converts: synchronous latency into observable backlog, and coupled scaling into independently sized fleets. Next: how one broker's log design delivers ordering and replay.

Kafka's Contract: Ordered Per Partition, Offsets Owned by the Consumer

RabbitMQ deletes a message after delivery; Kafka does not. A Kafka topic is a set of partitions, each an immutable, append-only log of records with monotonically increasing offsets. Nothing is consumed in a meaningful sense — a consumer group's progress is just a stored offset (__consumer_offsets), and reading means asking a broker for records at or after that position. That single design decision produces the properties teams adopt Kafka specifically for:

partition 7:  [0][1][2][3][4][5]...   append-only, retained for 7 days (or ∞)
                   ^ consumer group 'invoice-workers' last committed offset = 3
worker crash → resumes at 4 (or replays 3 if it didn't commit)
reset --to-earliest → reprocess from 0  — REPLAY without touching the broker
new consumer group → reads the whole retained history (backfill for free)

Ordering guarantee, stated precisely: records are ordered within a partition, not within a topic. A producer sending key order-42 gets the same partition every time (hash of key, or explicit partition assignment), so all events for one order are serialized — which is what makes per-entity state machines safe. Across partitions there is no order, because producers may be appending to different partitions concurrently. Therefore: ordering is a design choice expressed through the partition key. If you need global order, you need one partition, which means one writer-bottlenecked stream capped by one broker's throughput — a trade, not a feature.

Retention converts storage into capability. Records live until retention.ms (default 7 days) or retention.bytes, regardless of consumption, which enables the pattern library built on replay: rebuild a broken Elasticsearch index from topic beginning; add a new consumer (fraud model v2) and backfill a year of history; debug yesterday's incident by re-reading exactly what the service saw. The consumer offset commit cadence is the sharp edge: commit-after-processing gives at-least-once (crash between process and commit ⇒ the record is re-delivered); commit-before-processing gives at-most-once (crash after commit ⇒ the record is skipped). Neither is exactly-once by itself — exactly-once needs the transactional producer/broker or an idempotent sink.

Operational details that appear in real incidents:

  • Consumer groups and rebalancing. Group members split partitions; adding a consumer triggers a rebalance that revokes and reassigns partitions, during which nothing is consumed. session.timeout.ms/max.poll.interval.ms decide how long a stalled member blocks its partitions before being evicted — set too high, a hung consumer stalls a whole partition's pipeline for minutes.
  • Lag is per-partition, and one slow partition stalls that key's ordering guarantee. consumer lag = last produced offset - last committed offset; alert on time lag, not just count.
  • Skew: 64 partitions with keys like sequential user IDs can still cluster onto a few brokers if the key distribution is skewed — kafka-consumer-groups --describe shows it immediately.
  • The poison pill: a record that crashes the consumer replays forever from the committed offset. Fix with a retry topic or dead-letter queue after N attempts (RabbitMQ natively; Kafka via a DLT consumer), not by skipping the offset commit.

For interviews, the crisp formulation: Kafka gives you a durable, ordered, replayable log per partition with consumer-owned offsets; ordering scope = partition key; delivery semantics = your commit discipline plus idempotent effects. That sentence separates candidates who used Kafka from candidates who read about it. The alternative shape — broker-managed point-to-point delivery — is the subject of Pub/Sub vs Point-to-Point.

At-Least-Once in Practice: Ack Modes, Visibility Timeouts, and the Crash That Redelivers

A worker pulls message 714 from an SQS-style queue, charges the card, and the container is killed by the OOM reaper before it deletes the message. Five seconds later the visibility timeout expires and 714 is redelivered; the worker charges the customer twice. Nothing is malfunctioning — this is at-least-once delivery behaving exactly as specified. The acknowledgement mode decides what happens after a consumer crash; your code decides whether a duplicate is a bug.

The three modes, and the failure each one accepts:

Mode Broker removes message when After consumer crash Failure accepted
At-most-once on deliver (fire and forget) message already gone — work silently lost lost work
At-least-once on explicit ack unacked message becomes visible again → redelivered duplicate effects
Exactly-once on ack tied to an idempotent/transactional commit nothing re-delivered and nothing lost only with broker transactions or idempotent sinks

Exactly-once deserves skepticism in an interview: it is achievable end-to-end only when the effect is idempotent or transactional. Kafka transactions make produce-and-commit atomic within the broker; a consumer writing to MySQL needs an idempotency key (the record's (topic, partition, offset) tuple is ideal) with INSERT ... ON DUPLICATE KEY UPDATE, or an outbox. Design for at-least-once transport plus idempotent processing — that combination is what people colloquially call exactly-once, and it is the answer that survives follow-up questions.

The queue-side mechanics that create redelivery windows, named concretely:

SQS:          visibility timeout (default 30s) — message invisible while in flight,
              returns unless deleted. Extend with heartbeats (ChangeMessageVisibility)
              for long jobs; zero means immediately visible again.
RabbitMQ:     unacked messages are requeued on channel/connection close;
              nack/reject with requeue=true puts the message back (often at the head,
              which is how a poison message livelocks a consumer — use a DLX instead)
Kafka:        no broker-side ack per message; consumer commits offsets.
              Resume point = last committed offset ⇒ duplicates only for
              uncommitted-but-processed records

From that mechanics, the required consumer-side hygiene:

  1. Idempotency first. Deterministic operation IDs, dedupe tables with TTL, or natural keys (INSERT IF NOT EXISTS). Every at-least-once design starts here.
  2. Visibility/ack timeout ≥ worst-case job duration, with heartbeat extension — or you get redelivery while the original job is still running (two workers on the same job, not merely a retry).
  3. Dead-letter queues after N attempts with the failure reason preserved; a DLQ without an alarm and a replay tool is a landfill.
  4. Atomic side effect + ack. Commit the DB write and the offset/ack together where possible (Kafka consumer offset stored in the same transaction as the projection), otherwise ack first (loss) or effect first (duplication).
  5. Ordering under redelivery. RabbitMQ requeues to the head; Kafka resumes at an uncommitted offset — so downstream effects must tolerate seeing older messages after newer ones unless you process partitions serially.

In system-design interviews this topic shows up as 'how do you process payments without double-charging?'. The full answer: at-least-once delivery, idempotency keys on the effect, DLQ with alerting for the retry storm case, and a reconciliation job that compares ledger entries against provider records for whatever slips through. That same idempotency discipline is the prerequisite for the transactional workflows in The Saga Pattern.

Quiz

1. Traffic spikes 40x for 90 seconds while your workers can drain only 1.5x of normal rate. What does putting a queue in front actually change?

Question 1 options

2. Which statement about Kafka ordering is correct?

Question 2 options

3. A worker processes a message then crashes before committing its offset. What are the semantics, and what fixes the duplicate effect?

Question 3 options

4. An SQS consumer takes 90 seconds per job with the default 30-second visibility timeout and no changes. What happens?

Question 4 options

Flashcards

Question

What does a message queue convert, and what failure mode does it introduce?

Answer

It converts synchronous latency and coupled scaling into backlog depth and independent consumer sizing — bursts become lag instead of piled-up HTTP threads, and a consumer outage delays work instead of failing user requests. It introduces unconsumed-backlog risk: if sustained arrival exceeds drain rate, lag grows to retention limits and messages are lost, so consumer lag becomes the SLA to watch.

Question

Kafka ordering and replay in two sentences

Answer

Ordering is guaranteed within a partition (append-only offsets); key-based routing puts one entity's events in one partition, giving per-entity order, while cross-partition order is undefined. Consumers own offsets, so replay means rewinding a committed offset (or creating a new group) over retained records — the broker never deletes on read within retention.ms.

Question

What delivery semantics result from each offset-commit ordering?

Answer

Process then commit = at-least-once (crash between them redelivers). Commit then process = at-most-once (crash between them loses the record). Exactly-once is not a commit order — it requires broker transactions or an idempotent/transactional sink so a duplicate delivery has no additional effect.

Question

What is a visibility timeout, and what goes wrong when it is shorter than the job?

Answer

SQS's visibility timeout is a lease making an in-flight message invisible until deleted or the timer expires. If it expires before completion, the message returns to the queue and a second consumer processes it concurrently with the first — duplicate effects plus racing state. Fix: set it above worst-case duration or extend it with heartbeats while the job runs.

Question

Name the queue features that handle a poison message.

Answer

A retry limit plus dead-letter queue: after N failed attempts the message moves to a DLQ with its error metadata, an alarm fires, and a replay tool re-enqueues after a fix. RabbitMQ uses a dead-letter exchange (DLX) with TTL/max-length; Kafka apps implement a DLT topic in the consumer. Burying it by skipping offset commits silently drops work instead.

Revision Notes

Key Takeaways

  • 1. A queue trades immediate latency for visible backlog; the incident metric that matters becomes consumer lag relative to retention, not p99 alone
  • 2. Kafka's ordering scope is the partition — pick keys knowing that per-entity order requires co-location and that one hot key is one hot partition
  • 3. Commit discipline selects at-least-once or at-most-once; exactly-once only exists when the effect is idempotent or transactional
  • 4. Visibility timeouts and requeue paths create redeliveries by design, so long jobs need heartbeats and effects need dedupe keys
  • 5. Dead-letter queues need alarms and replay tooling, or they just hide the poison messages you were trying to surface

Interview Tips

  • • "What does a queue buy us?" — answer with the conversion (latency → lag, coupled scale → independent scale) and then immediately state the new obligation (retention vs drain rate) rather than only benefits
  • • "How does Kafka replay work?" — offsets are consumer state over retained logs: rewind the committed offset or add a new group; note ordering is per-partition and name the retention window
  • • "Payments must never double-charge" — at-least-once + idempotency key on (topic, partition, offset), DLQ with alerting, and a reconciliation job; mention commit ordering and why commit-before-lose data
  • • "A consumer keeps crashing on one message" — explain poison-pill redelivery from the uncommitted offset, then the retry-count/DLT pattern and why skipping commits drops work

Cheat Sheet

Message Queues and Brokers

What the buffer converts: peak arrival → backlog depth · caller latency → consumer lag · coupled scaling → independently sized fleets. Failure moves from HTTP cascade to growing lag; if drain rate < arrival rate, lag reaches retention and work is lost.

Broker shapes:

RabbitMQ  smart broker: routing, per-message ack, DLX, priority, TTL — task queues
Kafka     append-only log/partition, consumer-owned offsets, replay, 7d retention
SQS       managed: standard = at-least-once unordered · FIFO = dedup + group order
NATS      low-latency fan-out + core streams

Kafka contract:

order = per partition (key → same partition ⇒ per-entity order; no cross-partition order)
read  = from committed offset;  rewind/new group ⇒ replay from retention
commit-after-process ⇒ at-least-once · commit-before-process ⇒ at-most-once
lag   = last produced − last committed (ALERT ON TIME, not just count)

Ack / redelivery:

SQS    visibility timeout = lease; expires ⇒ redelivered (heartbeats for long jobs)
Rabbit unacked requeue on channel close; nack(requeue) → head → poison livelock ⇒ DLX
Kafka  no per-message ack; uncommitted offset ⇒ resume ⇒ duplicates possible

Rules: idempotent effects first (dedupe key = topic/partition/offset or natural key) · ack timeout ≥ job duration · DLQ after N attempts + alarm + replay · atomic effect+offset where possible · exactly-once = transport + idempotent sink, never a flag alone · watch consumer lag as the SLA · bound backlog (retention/disk) or producers get refused later.

Continue Learning