RabbitMQ for system design interviews
Open in the interactive app: practice, quiz, mock interview →In one minute
- RabbitMQ is a message broker: producers publish to an exchange, which copies each message into zero or more queues according to bindings; consumers take messages and acknowledge them.
- Routing is its distinguishing feature: direct, fanout, topic and headers exchanges express "who gets this" in broker configuration instead of application code.
- A queue is a single Erlang process. That gives simple per-queue FIFO semantics and caps one queue at roughly tens of thousands of messages per second.
- For durability, use quorum queues (Raft,
fsyncbefore confirm) with publisher confirms and manual acks. Delivery is at-least-once, so consumers must be idempotent. - Choose it for task queues and rich routing at moderate scale. Prefer Kafka or a stream for replay, very high fan-in, or multi-day retention.
How it works
Exchanges, bindings and queues
A producer never writes to a queue directly. It sends basic.publish with an exchange name and a routing key. A direct exchange matches the key exactly. A fanout exchange copies to every bound queue. A topic exchange treats the key as dot-separated words and lets binding patterns use * (exactly one word) and # (zero or more words). A headers exchange matches on header values. The nameless default exchange routes by queue name, which is why "publish to a queue" appears to work. If nothing matches, the message is dropped unless the producer set mandatory (the broker sends basic.return) or the exchange has an alternate exchange (AMQP 0-9-1 model).
Consumers, prefetch and acks
The broker pushes messages as basic.deliver frames, each with a per-channel delivery tag. With manual acks a message stays unacknowledged until basic.ack; basic.nack with requeue=true puts it back, and if the channel dies every unacked message is requeued with redelivered set. basic.qos sets the prefetch count, the maximum unacked messages per consumer. Too low and consumers idle on round trips; too high and one slow consumer hoards work. A delivery must also be acked within consumer_timeout, 30 minutes by default, or the broker closes the channel with PRECONDITION_FAILED and requeues everything it held; since 4.3 that timeout is enforced only for quorum queues (consumer docs).
Queue types and clustering
Classic queues live on one node and are not replicated: classic queue mirroring was removed in 4.0 (quorum queue docs). Quorum queues replicate through Raft, usually to three nodes. Streams are a replicated append-only log read by offset, giving replay inside RabbitMQ. Exchanges, bindings, users and policies are metadata replicated to every node, stored since 4.2 in Khepri, a Raft-based store that is the default for new clusters, while upgraded clusters stay on Mnesia until they opt in (metadata store docs); but queue contents live only on that queue's replicas, so a consumer connected to the wrong node has its traffic proxied across the cluster link.
Down the stack
Layer 1The contract
With a quorum queue, publisher confirms and manual acks, RabbitMQ promises: a confirmed message is persisted on a majority of replicas and survives losing a minority, because confirms are issued only after quorum replication (quorum queue docs); it will be delivered at least once until acked; and messages from one channel to one queue keep publish order, though redeliveries can arrive after later messages. It does not promise exactly-once delivery, ordering across queues, or ordering among competing consumers. Without confirms a publish is a TCP write the broker may drop.
Layer 2The mechanism
Wire protocol. AMQP 0-9-1 multiplexes channels over one TCP connection. Every frame is a 7-byte header (1-byte type, 2-byte channel, 4-byte size), the payload, and an end byte 0xCE. A publish is a basic.publish method frame, a content header frame (body size, delivery_mode=2 for persistent, message_id, headers) and body frames capped by frame_max (128 KiB default). The whole message is capped by max_message_size, 16 MiB by default (limits). After confirm.select, the broker replies with basic.ack carrying a delivery tag and a multiple bit, so one ack can confirm a range.
Routing and processes. Topic bindings live in a trie keyed by words with * and # edges, so routing cost grows with key depth, not binding count. Each connection has a reader and a writer process and one process per channel; the channel routes, then sends the message to each target queue's process. A classic queue is one process; a quorum queue is one Raft member process per replica.
Quorum queues. They use Ra, a Raft library. The leader appends the enqueue command and replicates append_entries to followers. Each node keeps one write-ahead log shared by all its Ra members, batching many queues into one fsync, then flushes entries into per-queue segment files. Once a majority is durable the command commits and the channel sends the confirm (Ra). Consumer acks are also log entries, and snapshots let the log be truncated.
Classic queues and streams. Classic queues (v2 storage) keep a per-queue index that embeds small messages, with larger ones in a node-wide shared message store. Streams write segment files with an offset and timestamp index and replicate leader to follower (Osiris).
Flow control. Queues grant channels credit; when it runs out the channel stops, and the reader stops reading the socket. A memory alarm above vm_memory_high_watermark (0.6 of detected RAM by default) (memory docs) or a disk alarm below disk_free_limit (only 50 MB by default, far too low for production) blocks all publishers cluster-wide (configuration).
Layer 3The machine
The Erlang VM runs one scheduler thread per core and preempts processes every few thousand reductions, so one busy queue cannot stall others but can never use more than one core. Sockets are polled with epoll. At 20,000 messages per second with confirms there are tens of thousands of tiny TCP segments each way, so multiple acks matter.
An fsync costs about 50 to 200 microseconds on NVMe and 1 to 5 ms on network block storage; the shared WAL lets 500 quorum queues pay for one fsync per batch. Raft adds a cross-zone round trip, typically 0.5 to 2 ms.
Memory is the main hazard. Each in-memory message costs its body plus a few hundred bytes of metadata, and each Erlang process garbage-collects its own heap. Rough single-queue ceilings (hedge: they vary widely with message size and hardware) are 30,000 to 50,000 messages per second for a classic queue and 10,000 to 30,000 for a quorum queue; streams can reach hundreds of thousands per second because consumers read files without per-message state.
Why it's built this way
- Routing belongs in one shared place, so the broker owns exchanges and bindings rather than producers knowing consumers.
- Erlang processes make per-queue state consistent without locks, so a queue is one process, bounded by one core; you scale by adding queues.
- A push broker must not overwhelm consumers, so prefetch and acks give per-consumer flow control and credit flow does the same internally.
- Classic mirroring lost data under partitions (Jepsen showed it), so durable replication moved to Raft queues that confirm after a majority
fsync, and 4.0 removed mirroring. fsyncand cross-zone round trips cost milliseconds, so the WAL is shared and confirms are batched.
Diagram
sequenceDiagram
participant P as Producer
participant C as Channel process
participant L as Queue leader (Ra)
participant F as Follower x2
participant K as Consumer
P->>C: basic.publish (key orders.eu.paid)
C->>C: topic trie lookup
C->>L: enqueue command
L->>L: append to WAL, fsync
L->>F: append_entries
F-->>L: ack after fsync
L-->>C: committed
C-->>P: basic.ack (confirm)
L->>K: basic.deliver (prefetch slot)
K-->>L: basic.ack (consumer)Worked example
An e-commerce platform emits order events: 10,000 per second at peak, about 2 KB each. Billing wants every paid order, shipping-eu wants EU paid orders, and analytics wants everything.
One topic exchange orders receives keys like orders.<region>.<status>. Billing binds orders.*.paid, shipping-eu binds orders.eu.paid, analytics binds orders.#. At peak that is 10,000 publishes but up to 30,000 queue writes, because one message can land in three queues.
Analytics at 10,000 per second would sit near one quorum queue's ceiling, so it is split into 4 queues behind a consistent-hash exchange keyed on order ID, about 2,500 per second each. Billing runs 20 consumers with prefetch 50; at 20 ms per message each consumer handles 50 per second, so 1,000 per second in total, above billing's share. Producers keep 500 unconfirmed messages in flight; at 5 ms confirm latency one channel could sustain 100,000 per second, so the queues, not confirms, are the limit. Billing deduplicates on message_id because redelivery after a consumer crash will eventually happen.
In the wild
- Jepsen, 2014: mirrored queues lose acknowledged messages. On a five-node cluster with durable mirrored queues, publisher confirms and
pause_minority, random partitions lost 1,312 of 3,747 attempted enqueues, about 35% of acknowledged writes, plus 24 duplicate deliveries. A node rejoining after a partition threw away its own queue contents and adopted the current primary's. This is the failure that quorum queues and the 4.0 removal of mirroring answer (Jepsen). - Slack, about 2016: a full broker cannot even drain. Not RabbitMQ, but the same shape as a memory alarm. Database contention slowed Slack's job workers until the Redis job queue hit its memory cap; enqueues failed, and dequeues failed too because moving a job to an in-flight list needed spare memory, so the queue stayed stuck after the database recovered. Slack put a Kafka buffer in front of Redis, with a relay that feeds jobs in at a rate limit, for about 1.4 billion jobs a day (Slack Engineering).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| RabbitMQ quorum queues | Rich routing, per-message acks, DLX, durable via Raft | One core per queue, messages deleted on ack, ms latency | Work queues and routed events at up to tens of thousands per second per queue |
| RabbitMQ streams | Replay, high throughput, offset consumers | Fewer routing features, newer operational model | You want RabbitMQ plus replay or large fan-out |
| Kafka | Partitioned log, millions per second, long retention | No per-message ack or routing, ordering only per partition | Event streaming, CDC, analytics pipelines |
| Amazon SQS | No servers, near-unlimited standard throughput | No routing, polling, 12-hour visibility max | AWS-native task queues with no ops team |
| Redis lists or streams | Very low latency, simple | Weaker durability, memory-bound | Lightweight jobs where loss is tolerable |
Pitfalls in production
- Unbounded queues trigger the memory alarm. A stalled consumer lets a queue grow until every publisher blocks. Set
x-max-lengthor a TTL withreject-publishoverflow and alert on depth. - Prefetch left unlimited. Without
basic.qosthe first consumer receives the whole backlog. Set prefetch to a small multiple of consumer concurrency, often 10 to 300. - Poison messages loop forever. Quorum queues cap redeliveries with
delivery-limit, 20 by default since 4.0 (unlimited in 3.x), then drop or dead-letter the message, so configure a dead-letter exchange or lose it silently (limits). - Fire-and-forget publishing. Without confirms, messages vanish during alarms and failovers. Treat an unconfirmed message as unsent.
- Connection churn. A connection per request costs TCP and AMQP handshakes. Keep long-lived connections with a few channels, one per thread.
- One giant queue. It pins one core. Shard with a consistent-hash exchange or use a super stream.
Leadership lens
- Check that confirms, quorum queues, manual acks and idempotent handlers are all present; missing any one weakens the whole chain.
- Treat queue depth as a primary SLO. A broker is healthy only while queues stay near empty.
- Decide early whether anyone will need replay; if so, a stream or Kafka avoids a later migration.
- Self-hosting means owning Erlang upgrades, partitions and disk alarms; Amazon MQ or CloudAMQP shifts that toil but not capacity planning.
Check yourself
Why can adding more consumers fail to raise the throughput of a single quorum queue past a certain point?
Show answer
The queue is one Raft leader process on one core doing enqueue, delivery and ack bookkeeping, so the queue itself becomes the bottleneck; you need more queues, not more consumers.
A producer publishes without confirms during a memory alarm. What happens to its messages?
Show answer
The broker blocks the connection and stops reading the socket, so messages sit in TCP buffers; if the producer times out or the connection drops they are lost without the producer knowing.
Does the binding
orders.#.paidmatch the keyorders.paid?Show answer
Yes, because
#matches zero or more words, so the pattern reduces toorders.paid.Why does RabbitMQ share one write-ahead log across all quorum queues on a node?
Show answer
fsyncis expensive, so batching appends from many queues into one log lets onefsyncmake many messages durable at once.
Sources and further study
Primary
- AMQP 0-9-1 specification (PDF): the frame format, method classes and exchange semantics RabbitMQ implements.
- In Search of an Understandable Consensus Algorithm (Raft): the consensus protocol under quorum queues and the Khepri metadata store.
Systems docs
- Ra: RabbitMQ's Raft library: the shared WAL, segment files and snapshots behind quorum queues.
- Metadata store (Khepri): what moved off Mnesia and how upgraded clusters migrate.
- Configurable limits and timeouts: current defaults for frame size, message size, consumer timeout and delivery limit.
Books
- Understanding Distributed Systems, 2nd ed., Vitillo, Ch 23 "Messaging" (channels, acknowledgements, duplicates, poison messages).
- Designing Distributed Systems, 2nd ed., Burns, Ch 11 "Work Queue Systems" and Ch 12 "Event-Driven Batch Processing" (Resiliency and Performance in Work Queues).
- Designing Data-Intensive Applications, 2nd ed., Kleppmann and Riccomini, Ch 12 "Stream Processing" (message brokers compared with log-based brokers).
Other curricula
- system-design-primer: lists RabbitMQ among message queues, with back pressure as the companion idea.
- Azure Architecture Center: Competing Consumers pattern: frames a work queue as scaling consumers, with idempotency, poison messages and ordering as the costs.
Watch
- RabbitMQ in 100 Seconds (Fireship): a fast orientation on exchanges, queues and bindings.
- RabbitMQ Crash Course (Hussein Nasser): AMQP connections, channels and the publish and consume flow, with code.
- What is RabbitMQ? (IBM Technology): where a broker fits compared with point-to-point calls.
Free resources
- AMQP 0-9-1 model explained: official guide to exchanges, bindings, routing keys and acknowledgements.
- Quorum queues: Raft replication, delivery limits, and when not to use them.
- Consumer acknowledgements and publisher confirms: the exact delivery guarantees and how to get them.
- Flow control: credit flow and the memory and disk alarms that block publishers.
- Streams: the append-only log type for replay and large fan-out.
- Jepsen: RabbitMQ: why the old mirrored queues lost data under partitions.