Design a Distributed Message Queue

Build a log-based broker: segmented logs on disk, partitions and ordering, leaders and in-sync replicas, acks, consumer groups, offsets and retention.

Case study33 minLesson 5 of 7
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

Dozens of services publish events: orders placed, payments taken, pages viewed. Dozens more read them at their own pace, and some want to reread last Tuesday's events after fixing a bug. You are asked to build the broker in the middle: a cluster that accepts a hundred thousand messages a second, keeps them for a week, and does not lose an acknowledged message when a broker dies. It is the kind of product people mean when they say Kafka.

Interviewers ask this to see what you know under the abstraction. How to use a queue (queue versus log, consumer groups, backpressure) is in Message Queues and Streams. This lesson builds the log itself: files on disk, replicas, offsets, and the exact meaning of "acknowledged". The steps follow A Framework for Any Design.

Clarify the problem

The first five minutes
Interviewer

Design a distributed message queue.

You

Is it a job queue, where each message goes to one worker and is deleted, or a log that several groups read independently and can replay?

Interviewer

A log. Several teams read the same events, and they want to replay.

You

Then order matters too. Do consumers need order across all events, or per key, like per order ID? And what scale and retention?

Interviewer

Per key is enough. 100,000 messages a second on average, three times that at peak, about 1 KB each, kept for seven days. Each message is read by about three groups.

You

Then my priorities: an acknowledged message is never lost, even if a broker dies; order holds within a key; producers get an answer in a few milliseconds; and consumers can rewind. I'll leave out per-message delays, priorities and cross-region replication for now.

  • Functional: produce messages to a topic, with an optional key; consume in groups, where each group gets every message and its members share the work; commit and rewind positions; delete data after a retention period, or keep only the latest value per key.
  • Non-functional: no acknowledged message is lost when one broker dies; order is kept per key; produce p99 under about 10 ms; 300,000 messages a second at peak; 7 days of retention; consumers can be hours behind without slowing producers.
  • Out of scope: delayed and priority messages, per-message acknowledgement, replication between regions.

Estimate the scale

  • Ingest: 100,000 × 1 KB = 100 MB/s on average, 300 MB/s at peak.
  • Per day: 100 MB/s × 86,400 s ≈ 8.6 TB.
  • Reads: three groups read everything, so 300 MB/s out on average and 900 MB/s at peak, plus replica traffic.
Estimate: Disk for seven days

How much disk does the cluster need to keep 7 days of messages with 3 copies of each?

You know
  • 100 MB/s of messages on average
  • Kept for 7 days
  • 3 copies of every message

Show the working
  1. 100 MB/s × 86,400 s ≈ 8.64 TB a day
  2. 8.64 TB × 7 ≈ 60 TB
  3. 60 TB × 3 copies ≈ 180 TB

Estimate 180 TB (anything from 60 TB to 600 TB is a good answer)

With 12 TB of usable disk per broker, 180 ÷ 12 = 15 brokers with full disks. Keep disks under about 75% full: 20 brokers. Disk, not CPU, sets the cluster size.

Check the disks of 20 brokers at peak: 300 MB/s × 3 copies ÷ 20 = 45 MB/s of writes each, and 900 MB/s ÷ 20 = 45 MB/s of consumer reads. Comfortable for sequential I/O.

Estimate: Partitions for the busiest topic

The busiest topic peaks at 40,000 messages a second. One consumer instance handles 2,500 a second. How many partitions does it need today?

You know
  • 40,000 messages a second at peak
  • One consumer handles 2,500 a second
  • Within a group, one partition is read by one consumer at a time

Show the working
  1. 40,000 ÷ 2,500 = 16 consumers busy at peak
  2. Each needs its own partition, so at least 16
  3. Plan for growth: double it to 32

Estimate 16 (anything from 8 to 64 is a good answer)

Partitions are hard to add later without breaking per-key order, so choose for next year's load.

Your turn: Define the API

Write the calls for producing, consuming and saving a position. How does a consumer say where to read from, and what does a producer learn when it sent a message to the wrong broker?

Compare with a model answer

Produce returns the offset where the batch landed. Fetch takes a partition and an offset, so the consumer, not the broker, owns its position. A commit call saves that position for the group. Errors tell clients to refresh their metadata when leadership has moved.

Define the API

Broker protocol (binary over TCP)
Metadata(topics)
  -> per partition: leader broker, replicas, in-sync replicas
 
Produce(topic, partition, batch, acks = 0 | 1 | all,
        producer_id, sequence)
  OK base_offset=5000
  NOT_LEADER                 leadership moved; refresh metadata, retry
  NOT_ENOUGH_REPLICAS        fewer in-sync copies than required
  DUPLICATE_SEQUENCE         already written; returns the first offset
 
Fetch(topic, partition, offset, max_bytes, max_wait_ms)
  records from offset up to the high watermark
  OFFSET_OUT_OF_RANGE        that data was deleted by retention
 
JoinGroup(group, member) / Heartbeat(group, member, generation)
  -> assigned partitions, generation=12
 
CommitOffset(group, generation, partition, offset)
  OK
  ILLEGAL_GENERATION         you were removed from the group; rejoin

The decision worth arguing is pull versus push. Consumers pull: each one asks for the next bytes from its own offset, at its own pace. A slow consumer then falls behind without slowing anyone, a consumer can batch large reads, and rewinding is just asking for an older offset. To avoid busy polling, fetch waits up to max_wait_ms for data before answering empty.

Your turn: Model the data

How does a broker store a partition on disk so that appends are fast, reads from any offset are fast, and deleting old data is cheap? What metadata must the whole cluster agree on?

Compare with a model answer

Each partition is a directory of segment files, each named by its first offset, plus a small sparse index per segment. Appends go to the newest segment; retention deletes whole old segments. The cluster agrees on which brokers hold each partition, which is the leader, and which replicas are in sync.

Model the data

One partition on disk, and the cluster metadata
orders-7/                             partition 7 of topic "orders"
  00000000000004000000.log            closed segment, about 1 GB
  00000000000004000000.index          offset -> byte position, every 4 KB
  00000000000005000000.log            active segment: appends go here
  00000000000005000000.index
 
record batch (as sent, as stored, as served)
  base_offset, producer_id, sequence, crc
  records: offset_delta, timestamp, key, value, headers
 
cluster metadata                      held by the controller
  topic -> partitions
  partition -> replicas [1, 2, 3], leader 1, leader_epoch 4,
               in_sync [1, 2, 3]
 
consumer offsets                      an internal topic, compacted
  (group, topic, partition) -> next offset to read

There is no database here. The broker's own files are the store, and the only access patterns are "append at the end" and "read forward from offset X". Consumer positions are themselves messages, stored in an internal topic that keeps only the latest value per key. The small, critical metadata lives in a controller replicated with consensus.

Your turn: High-level design

Sketch one partition with three replicas. Who does the producer talk to, how do the other copies get the data, and who decides which broker leads?

Compare with a model answer

The producer writes to the partition's leader. Followers fetch from the leader, the same way consumers do. Consumers read from the leader too. A controller quorum records leaders and in-sync replicas and picks a new leader when one dies.

High-level design

Every partition has one leader and two followers on different brokers. Each broker leads about a third of its partitions. The picture shows one partition.

For one partition: producers and consumers talk to the leader; followers pull from the leader; the controller quorum records who leads and who is in sync.Producers to Broker 1 (leader) (produce); Consumer group to Broker 1 (leader) (fetch, commit); Broker 2 (follower) to Broker 1 (leader) (fetch); Broker 3 (follower) to Broker 1 (leader) (fetch); Controller quorum to Broker 1 (leader) (leader, ISR).ProducersConsumer groupBroker 1(leader)Broker 2(follower)Broker 3(follower)Controllerquorumproducefetch, commitfetchfetchleader, ISR
For one partition: producers and consumers talk to the leader; followers pull from the leader; the controller quorum records who leads and who is in sync.

Step through one message with acks=all, a consumer reading it, then the leader dying in the middle of a retry.

Produce, replicate, consume, then the leader diesUse the arrows or ← → keys
  1. Produce, replicate, consume, then the leader dies, step 1Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)Broker 2(follower)Broker 3(follower)Controllerquorum
    end of log
    5000
    high watermark
    5000

    1A producer hashes key order-881 to partition 7, whose leader is broker 1, and sends a batch with acks=all, producer id 31, sequence 42.

  2. Produce, replicate, consume, then the leader dies, step 2Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)offset 5000Broker 2(follower)Broker 3(follower)Controllerquorum
    end of log
    5001
    high watermark
    5000

    2Broker 1 appends the batch at offset 5,000 in its active segment. It is in the page cache of one machine only, so the producer waits.

  3. Produce, replicate, consume, then the leader dies, step 3Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)Broker 2(follower)has 5000Broker 3(follower)has 5000Controllerquorum
    end of log
    5001
    high watermark
    5000

    3Followers pull: brokers 2 and 3 each fetch from offset 5,000 and append the same bytes to their own logs.

  4. Produce, replicate, consume, then the leader dies, step 4Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)Broker 2(follower)Broker 3(follower)Controllerquorum
    end of log
    5001
    high watermark
    5001

    4Their next fetches ask for offset 5,001, which tells the leader they hold 5,000. All in-sync replicas have it, so the high watermark moves to 5,001 and the producer gets OK.

  5. Produce, replicate, consume, then the leader dies, step 5Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupread 5000Broker 1(leader)Broker 2(follower)Broker 3(follower)Controllerquorum
    high watermark
    5001
    billing offset
    5000

    5A billing consumer fetches from its committed offset 5,000. Consumers see only records below the high watermark, which every in-sync replica holds.

  6. Produce, replicate, consume, then the leader dies, step 6Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)Broker 2(follower)Broker 3(follower)Controllerquorum
    high watermark
    5001
    billing offset
    5001

    6It processes the order, then commits offset 5,001: "next, read 5,001". Committing after the work gives at-least-once delivery.

  7. Produce, replicate, consume, then the leader dies, step 7Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)downBroker 2(follower)has 5001Broker 3(follower)has 5001Controllerquorum
    high watermark
    5001
    billing offset
    5001

    7The producer sends sequence 43 to offset 5,001. Both followers copy it, but broker 1 dies before it sends the OK. The producer times out and does not know if the write landed.

  8. Produce, replicate, consume, then the leader dies, step 8Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)downBroker 2(follower)leaderBroker 3(follower)Controllerquorumepoch 5
    leader
    broker 2
    leader epoch
    5

    8Broker 1 stops heartbeating. After a few seconds the controller picks broker 2, which is in sync, as leader and raises the leader epoch from 4 to 5.

  9. Produce, replicate, consume, then the leader dies, step 9Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)downBroker 2(follower)dup seq 43Broker 3(follower)Controllerquorum
    leader
    broker 2
    copies of seq 43
    1

    9The producer refreshes metadata and retries sequence 43 at broker 2. Broker 2 already holds producer 31's sequence 43, so it returns the original offset 5,001 instead of writing a second copy.

  10. Produce, replicate, consume, then the leader dies, step 10Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupread 5001Broker 1(leader)downBroker 2(follower)Broker 3(follower)Controllerquorum
    leader
    broker 2
    billing offset
    5001

    10The consumer group also moves to broker 2 and resumes at offset 5,001. Every acknowledged record was on every in-sync replica, so nothing is lost.

  11. Produce, replicate, consume, then the leader dies, step 11Producers to Broker 1 (leader); Consumer group to Broker 1 (leader); Broker 2 (follower) to Broker 1 (leader); Broker 3 (follower) to Broker 1 (leader); Controller quorum to Broker 1 (leader).ProducersConsumer groupBroker 1(leader)truncate, followBroker 2(follower)Broker 3(follower)Controllerquorum
    leader
    broker 2
    leader epoch
    5

    11Broker 1 restarts. Any records it wrote past what broker 2 had at the epoch change are cut off, then it fetches from broker 2 as a follower until it is back in sync.

1 / 11

Deep dives

The log on disk

A random 1 KB write on a spinning disk costs a seek of about 10 ms, so 100 writes a second, or 100 KB/s. Appending to the end of a file, the same disk streams about 200 MB/s, 2,000 times more. SSDs narrow the gap, but large sequential writes still mean fewer, bigger operations. So the broker only appends: no updates in place, no per-message index.

  • Batching. Producers group messages for a few milliseconds, say 16 KB at a time. At 100,000 messages a second that is about 6,250 requests instead of 100,000, and the batch is compressed as a unit.
  • The page cache. The broker writes to the operating system's file cache and does not force each write to disk. Consumers that keep up read data that is still in memory, so most reads never touch the disk.
  • Zero copy. Because stored bytes equal sent bytes, the broker can ask the kernel to send a file range straight to the socket without copying it through the program.

Segments and the index. A partition is split into segment files of about 1 GB, each named after its first offset. To serve "read from offset 4,712,345", the broker picks the segment by name, then uses a sparse index with one entry per 4 KB of log: a 1 GB segment has about 250,000 entries, 2 MB, kept in memory. Binary search finds the nearest entry in about 18 steps, then the broker scans forward less than 4 KB.

Retention and compaction. Deleting old data means deleting whole closed segments: a file delete, not a rewrite. A segment goes once its newest record is older than 7 days. For topics where only the latest value per key matters, such as "current address of user 42", use compaction instead: a background job rewrites old segments keeping only the last record for each key. A record with an empty value, a tombstone, deletes the key, and is itself removed after a grace period so slow readers still see the delete. The consumer offsets topic is compacted this way.

Replication and what "acknowledged" means

Followers fetch from the leader like consumers. The leader tracks which followers are caught up, within say 10 seconds: the in-sync replicas (ISR). The high watermark is the highest offset every ISR member has, and only records below it are visible to consumers. The producer chooses how long to wait with acks:

What the producer waits for
OptionGood atBad atPick it when
acks=0Lowest latency; no waiting.Any network error or broker crash loses messages silently.Metrics or logs where a small loss is fine.
acks=1One round trip to the leader, about 1 to 2 ms in a data center.If the leader dies before followers copy the record, an acknowledged message is lost.High-volume events where losing a few on a failover is acceptable.
acks=all, min ISR 2 Usual defaultAn acknowledged record is on every in-sync replica, at least 2. One broker can die with no loss.Waits for the slowest in-sync follower. With only 1 replica in sync, writes are refused.Orders, payments, anything that must not vanish. The usual default here.

Be exact about the guarantee. With 3 replicas, acks=all and a minimum ISR of 2, an acknowledged record is in the log of at least 2 brokers. "In the log" means the page cache, not necessarily the disk: durability comes from copies failing independently, so put the 3 replicas in different racks or zones. The minimum matters: without it, the ISR could shrink to the leader alone, and acks=all would quietly mean acks=1.

The controller. A quorum of 3 controller nodes keeps the metadata in a consensus-replicated log. It notices dead brokers by missed heartbeats, picks a new leader from the ISR, and records every ISR change. Each new leader gets a higher leader epoch, and brokers reject requests carrying an older one. This closes the zombie-leader race: an old leader cut off from the controller cannot remove followers from the ISR, so it cannot acknowledge an acks=all write alone. With acks=1 it can, and those writes are cut off when it rejoins.

If every ISR member dies, either wait for one to return (unavailable, no loss) or let an out-of-sync replica lead (available, acknowledged records lost). Wait, by default.

Ordering and producing without duplicates

Order is per partition, and the partition is chosen by hashing the key, so all of order-881's events stay in order. Two retries can break even that: batch 42 fails, batch 43 succeeds, then 42 is retried and lands after 43. A retry after a lost acknowledgement, as in the tracer, writes a duplicate.

The idempotent producer fixes both. Each producer gets an id, and numbers its batches per partition. The broker remembers the last few sequence numbers per producer: a repeat is answered with the original offset, and a gap (43 before 42) is rejected so the producer resends in order. This covers retries within one producer session: a restarted producer gets a new id, so its resends slip through unless it keeps a stable id. The cost is a little state per producer on each broker (the general idea is in Idempotency and Retries).

Consumer groups, offsets and delivery

One broker acts as the group coordinator. Members send heartbeats; when one joins, leaves or misses heartbeats, the group rebalances and partitions are reassigned. Each assignment has a generation number, and offset commits from an old generation are rejected, so a consumer that paused for a minute cannot overwrite the position of the one that took its partitions.

The simplest rebalance stops every member and reassigns everything, pausing all 32 partitions to move one. A cooperative rebalance pauses only the partitions that change owner. Prefer it.

Delivery depends on when the offset is committed. Commit after processing for at-least-once: if offsets are committed every 5 seconds and a consumer dies, up to 5 × 2,500 = 12,500 of its messages are processed again. For exactly-once within the log, a consumer that reads, transforms and writes to another topic can commit its output and its offsets in one transaction; readers that only read committed data never see a half-done result. That promise ends at the log: an email or a database write outside it still needs idempotent processing.

Bottlenecks and failures

At 10x, 1 GB/s in and 1.8 PB for a week, disks run out first: 1.8 PB ÷ 12 TB = 150 brokers, about 200 with headroom. Move closed segments older than a day to cheaper object storage and keep only recent ones on local disk. Partition count grows too, and a failed broker leading thousands of partitions takes longer to fail over. Hot keys stay hot partitions; more brokers do not split them. A consumer replaying last week reads from disk and evicts recent data from the page cache, slowing readers that were keeping up: cap replay bandwidth per client.

When this diesWhat happensWhat you do
A leader brokerIts partitions refuse writes for a few seconds.The controller elects ISR members as leaders; producers retry without duplicates.
A followerIt drops out of the ISR; writes continue with 2 in sync.Replace it; it copies the log and rejoins. A second loss in that partition blocks writes, not reads.
A controller nodeNothing, with 2 of 3 alive.Replace it. Losing 2 freezes leader changes; existing leaders keep serving.
A slow consumerLag grows; producers are unaffected.Alert on lag. Past 7 days its offset is deleted and it gets OFFSET_OUT_OF_RANGE.
A whole zoneOne replica of each partition is gone.Spread replicas over 3 zones, so 2 in-sync copies remain.

What a strong answer sounds like

LevelWhat the answer does
JuniorDraws producers, a broker and consumers, stores messages in a database table, and deletes them once read.
MidUses an append-only log with partitions and offsets, sizes disks from retention, adds replicas, and knows order is per partition.
SeniorExplains why sequential writes are fast, defines the ISR and the high watermark, states exactly what acks=all guarantees and when it does not, fences zombies with epochs and generations, and says what exactly-once does not cover.
What does acks=all promise?Strong answer
Interviewer

With acks=all, can an acknowledged message be lost?

You

With 3 replicas and a minimum ISR of 2, the record is in the log of at least 2 brokers before the OK. Losing it takes both failing before either has flushed, so I put replicas in separate zones. Two ways to lose it anyway: allowing an out-of-sync replica to become leader, or setting the minimum to 1, which turns acks=all into acks=1 when followers lag.

Interviewer

Why not fsync every message?

You

It would cap each disk at its sync rate and add milliseconds to every write. Copies on independent machines buy more safety per millisecond.

Follow-up questions

Why not use a database table as the queue?

Every consume becomes an update or delete, indexes grow and shrink, and many consumers fight over the same rows. A log only appends, and each group's position is one number per partition.

How do you add partitions to a busy topic?

Adding partitions changes hash(key) mod n, so a key's new events land in a different partition from its old ones, and order breaks for that moment. Pick the count for future load, or create a new topic and migrate consumers.

How would you keep data for a year?

Tier it: recent segments on broker disks, older ones in object storage, fetched on demand. Old reads are slower; brokers stay small.

How do you support many small topics?

Each partition costs open files, index memory and metadata. Share partitions across tenants with a key prefix, or cap partitions per broker.

How would you run it in two regions?

Keep one cluster per region and copy chosen topics asynchronously. Offsets differ between clusters, so map them by timestamp when a consumer fails over, and expect some duplicates.

Self-review: distributed message queue0 of 13

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure