CMD Guide
HomeSystem DesignSystem Design Problems

Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced

Building the thing every other design uses

Almost every system design in this guide reaches for "add a message queue." This page designs the queue itself — which is where you discover that the properties everyone assumes (durability, ordering, exactly-once) are not free, and that each one is bought by giving up something specific.

Requirements worth stating

Two messaging models

Point-to-point: each message is consumed by exactly one consumer, then deleted — a work queue. Publish-subscribe: each message goes to every subscriber, and the message is retained independently of who has read it.

The design that generalizes both is a retained log with per-consumer-group offsets. Messages are appended to a log and kept for the retention period; each consumer group tracks its own read position. Point-to-point is then one consumer group; pub-sub is several groups reading the same log. That is a genuinely important unification: the broker stops managing per-message state (which is what makes traditional queues hard to scale) and manages a cursor per group instead — the same trick as the chat sync cursor.

Storage: why a write-ahead log and not a database

The obvious implementation is a table of messages with a "consumed" flag. It fails at scale for a specific reason: random reads and writes plus per-message mutation, on the highest-volume path in your infrastructure.

Instead, each partition is an append-only log segmented into files. Writes are sequential appends — the fastest thing a disk does, and roughly comparable to memory throughput for streaming writes. Reads are sequential scans from an offset. Nothing is mutated; deletion happens by dropping whole old segments. Consumers do not remove messages, they advance an offset.

This buys three things at once: throughput (sequential I/O), replay (rewind the offset and reprocess — a superpower for bug recovery and for adding a new consumer to existing data), and simple durability (the log is the write-ahead log). The cost: you store messages for the full retention window whether or not anyone reads them, and a slow consumer's problem shows up as an offset lag rather than as a growing queue.

Two rows show the same failure, a leader dying immediately after acknowledging, under different ack rules. With acks=1 the leader acknowledges message 7 before replicating, then dies while both followers still hold only message 6, so the new leader has message 6 and message 7 is gone even though the producer believes it was durable. With acks=all the leader replicates to both in-sync followers, which both hold message 7, and only then acknowledges, so the new leader still has message 7 and nothing is lost at the cost of waiting for the slowest in-sync replica. A note explains that the in-sync replica set can shrink to just the leader, which is why a minimum-ISR floor rejects writes rather than acknowledging data one disk can lose.
Two rows show the same failure, a leader dying immediately after acknowledging, under different ack rules. With acks=1 the leader acknowledges message 7 before replicating, then dies while both followers still hold only message 6, so the new leader has message 6 and message 7 is gone even though the producer believes it was durable. With acks=all the leader replicates to both in-sync followers, which both hold message 7, and only then acknowledges, so the new leader still has message 7 and nothing is lost at the cost of waiting for the slowest in-sync replica. A note explains that the in-sync replica set can shrink to just the leader, which is why a minimum-ISR floor rejects writes rather than acknowledging data one disk can lose.

Partitions: the unit of parallelism and of ordering

A topic is split into partitions, each an independent log on (potentially) a different broker. This is the central design decision, and it makes one trade that must be understood clearly:

Ordering is guaranteed within a partition, and nowhere else. There is no total order across a topic, because that would require a single serialization point and destroy the parallelism partitions exist to provide. So if you need events for one entity in order, they must go to the same partition — which you arrange by partitioning on a key (hash(user_id) % partitions). Order per user, parallelism across users.

Consequences worth knowing before you rely on them:

Replication and the ISR: where durability actually lives

Each partition has a leader and followers. Producers write to the leader; followers replicate. The set of replicas currently caught up is the in-sync replica set (ISR). A follower that falls too far behind is removed from the ISR so it cannot hold up writes.

The producer's acks setting decides when a write is acknowledged:

The subtlety that catches people: acks=all is only as strong as the ISR is large. If followers fall behind and get evicted, the ISR can shrink to just the leader, at which point "all in-sync replicas" means one machine — and acks=all silently degrades to acks=1. The guard is a minimum-ISR floor: if the ISR drops below it, reject the write rather than acknowledge something a single disk failure can erase. That is a deliberate choice of unavailability over silent data loss, and it is the correct one for a durable queue.

Delivery semantics: the three options, honestly

What real systems provide is exactly-once processing, built from at-least-once delivery plus one of two mechanisms: producer idempotence (each producer stamps a sequence number so the broker discards a duplicate append) and transactional commits (the message's effect and the consumer's offset advance commit atomically, so reprocessing cannot double-apply). Both narrow the guarantee to a scope you control.

The practical advice is the one people skip: make consumers idempotent and stop trying to buy exactly-once from the broker. A consumer whose handler is keyed by message ID turns at-least-once into effectively-once at a fraction of the cost and complexity, and it survives failure modes the broker's transactions do not cover (such as your handler writing to an external system that has never heard of the transaction).

Consumer groups and rebalancing

Consumers in a group divide the partitions between them: each partition is owned by exactly one consumer in the group, which is what preserves per-partition ordering while allowing parallel consumption. Group membership is coordinated by a broker acting as group coordinator (older designs used ZooKeeper for this).

Rebalancing reassigns partitions when a consumer joins, leaves, or stops heartbeating. It is necessary and it is disruptive: in the simple protocol, all consumers stop consuming while the assignment is recomputed — a stop-the-world pause. Two consequences follow:

The extras that get requested

Which setting, when

DecisionOptionChoose whenCost / breaks when
Durabilityacks=all + min-ISRPayments, orders, anything you cannot loseLatency tracks the slowest replica; writes rejected when ISR shrinks
Durabilityacks=1Logs, clickstream where rare loss is tolerableLeader failure loses acknowledged messages — silently
Durabilityacks=0Metrics samples, fire-and-forget telemetryAnything with business meaning
SemanticsAt-least-once + idempotent consumerNearly always — the pragmatic defaultRequires a dedupe key and somewhere to store it
SemanticsTransactional exactly-onceStream processing wholly inside one systemExternal side effects; throughput and complexity cost
OrderingKey-partitionedPer-entity order with cross-entity parallelismHot keys pin to one partition; repartitioning breaks history
OrderingSingle partitionTotal order genuinely requiredThroughput capped at one broker, one consumer
ThroughputLarge batchesBulk pipelines, analytics ingestionLatency-sensitive paths — messages wait to fill a batch

Pitfalls

Cost model — what dominates the bill

A message queue's cost is replicated storage plus cross-zone network, and the replication factor multiplies both.

Rough BOTE: 500,000 messages/second at 1 KB each is 500 MB/s ≈ 43 TB/day of ingress. With a 7-day retention that is ~300 TB stored, and at replication factor 3 that is ~900 TB provisioned. On SSD-backed storage at roughly $0.10/GB-month, ~900 TB is on the order of $90,000/month — which is why retention is the most consequential setting in the whole system, and why tiered storage (recent segments on SSD, older segments on object storage at ~$0.02/GB-month) is now standard: it can cut that line by 4–5× for the cold majority.

Then the network. Replication factor 3 across three availability zones means every message crosses a zone boundary twice. At 500 MB/s, that is ~1 GB/s of inter-zone traffic; at typical $0.01/GB inter-zone rates that is roughly $26,000/month in network charges alone. Add consumer fan-out: each additional consumer group re-reads the full stream, so five groups means 5× the egress on the read side.

Dominant line items: replicated retained storage; inter-zone replication network; then read egress multiplied by the number of consumer groups.

Levers: shorten retention (largest single effect, bounded by your recovery requirements); tiered storage to object storage for older segments; compression, which cuts storage and both network legs at once and is nearly free given batching already groups messages; and rack/zone-aware consumer placement so consumers read from a local replica rather than across a zone.

Operability: the fingerprints of a sick queue

Consumer lag growing linearly is the base case — consumers cannot keep up — but the shape matters: lag on one partition while others are flat is a hot key or a poison message, and adding consumers will not help either. Lag sawtoothing with periodic collapse to zero is a rebalance storm: consumers pause, lag builds, they resume and catch up, then a timeout triggers another rebalance.

ISR shrinking on one broker is a leading indicator of the durability problem, and it is the metric most worth alerting on, because it silently weakens acks=all before any data is lost. Under-replicated partitions that never recover mean a follower cannot catch up — often a slow disk — and it will stay out of the ISR, leaving that partition one failure from loss.

The nastiest fingerprint is consumer lag approaching the retention window: past that point the broker deletes data the consumer has not read, and the loss is permanent and unannounced. It should be an alert, not a dashboard. Watch also for throughput dropping while CPU and disk look idle, which usually means batch sizes collapsed (a producer config change) so per-message overhead now dominates, and duplicate processing after every deploy, which means offsets are committed on a timer rather than after successful processing.

Signals worth having: consumer lag per partition (not just per topic), ISR size and under-replicated partition count, lag-versus-retention headroom, rebalance frequency, average batch size, dead-letter queue arrival rate, and per-partition throughput skew.


Authored for this guide to cover designing a distributed message queue (Alex Xu Vol. 2, ch. 19 — not present in the Vol. 1 PDF); acks/ISR failure-trace diagram hand-authored as SVG. Complements this guide's Kafka topic (partitions, offsets, consumer groups, RabbitMQ vs Kafka vs ActiveMQ) by designing the broker's durability and delivery guarantees from first principles.

🤖 Don't fully get this? Learn it with Claude

Stuck on Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced** (System Design) and want to truly understand it. Explain Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Designing a Distributed Message Queue — Partitions, ISR & Delivery Semantics, Traced** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes