Engineering Atlas › System Design Interviews

RabbitMQ for system design interviews

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

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

  1. Routing belongs in one shared place, so the broker owns exchanges and bindings rather than producers knowing consumers.
  2. Erlang processes make per-queue state consistent without locks, so a queue is one process, bounded by one core; you scale by adding queues.
  3. A push broker must not overwhelm consumers, so prefetch and acks give per-consumer flow control and credit flow does the same internally.
  4. 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.
  5. fsync and 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

Trade-offs

OptionStrengthsWeaknessesChoose when
RabbitMQ quorum queuesRich routing, per-message acks, DLX, durable via RaftOne core per queue, messages deleted on ack, ms latencyWork queues and routed events at up to tens of thousands per second per queue
RabbitMQ streamsReplay, high throughput, offset consumersFewer routing features, newer operational modelYou want RabbitMQ plus replay or large fan-out
KafkaPartitioned log, millions per second, long retentionNo per-message ack or routing, ordering only per partitionEvent streaming, CDC, analytics pipelines
Amazon SQSNo servers, near-unlimited standard throughputNo routing, polling, 12-hour visibility maxAWS-native task queues with no ops team
Redis lists or streamsVery low latency, simpleWeaker durability, memory-boundLightweight jobs where loss is tolerable

Pitfalls in production

Leadership lens

Check yourself

  1. 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.

  2. 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.

  3. Does the binding orders.#.paid match the key orders.paid?

    Show answer

    Yes, because # matches zero or more words, so the pattern reduces to orders.paid.

  4. Why does RabbitMQ share one write-ahead log across all quorum queues on a node?

    Show answer

    fsync is expensive, so batching appends from many queues into one log lets one fsync make many messages durable at once.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →