awesome-everything RU
↑ Back to the climb

Queues, Streams, Eventing

RabbitMQ exchanges: the routing layer that decides who gets a copy

Crux Producers never publish to a queue — they publish to an exchange, and bindings plus routing keys decide which queues get a copy. Pick the wrong exchange type and messages either fan out everywhere or vanish silently.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at junior altitude — the surface
◷ 16 min

A payments service goes live. Orders flow, but the fraud team’s queue is empty — zero messages, no errors, no exception in any log. The producer is publishing happily; the broker is accepting. Three days later someone notices: the new fraud queue was never bound to the exchange. RabbitMQ routed every message to nowhere and dropped it without a sound. The publish call returned success the whole time, because by default an exchange with no matching binding silently discards the message.

You publish to an exchange, not a queue

The first surprise for anyone arriving from a job-queue mental model: a RabbitMQ producer cannot send a message to a queue. It sends to an exchange, and the exchange decides — using its type, the message’s routing key, and the bindings that connect it to queues — which queues (if any) receive a copy. A binding is a rule that says “exchange X, deliver to queue Q when the routing key matches pattern P.” One message can land in zero queues, one queue, or many; each matched queue gets its own independent copy.

This indirection is the whole point. The producer knows nothing about consumers, queues, or how many of them exist. You can add a new consumer — that fraud queue — by creating a queue and binding it, with zero changes to the producer. That decoupling is why RabbitMQ is described as a smart broker: routing logic lives in the broker, not in the producer or consumer.

The four exchange types are four routing strategies

Every exchange has a type, and the type is the routing algorithm. There are four you will actually use:

TypeRoutes when…Use it for
directrouting key exactly equals the binding keypoint-to-point work routing (payments.refund → one worker pool)
fanoutalways — routing key is ignoredbroadcast: every bound queue gets a copy (cache invalidation, pub/sub)
topicrouting key matches a dotted pattern with * / #selective subscription (order.*.eu, logs.#)
headersmessage header attributes match the binding’s argsmulti-attribute routing where a flat key isn’t enough

Direct is exact-match: bind queue Q with key payments.refund, and only messages whose routing key is exactly payments.refund reach it. Fanout ignores the routing key entirely and copies every message to every bound queue — the cheapest, fastest path, ideal when N independent consumers all need the same event. Topic is the workhorse: routing keys are dot-delimited words (order.created.eu), and a binding pattern uses * to match exactly one word and # to match zero or more. So order.*.eu catches order.created.eu but not order.created.us; logs.# catches everything under logs. Headers ignores the routing key and matches on a map of header attributes, with an x-match of all or any — powerful but slower and rarely worth it; a topic key usually expresses the same intent more cheaply.

Why this works

A topic exchange with a binding key of # behaves exactly like a fanout (matches everything), and a topic with no wildcards behaves like a direct. People sometimes “just use topic for everything.” It works, but fanout and direct are simpler to reason about and marginally faster because they skip pattern matching — reach for the specific type when the intent is specific.

Smart broker, dumb consumer — and why that’s the opposite of Kafka

This is the senior framing that decides whether RabbitMQ even fits your problem. RabbitMQ pushes messages to consumers and deletes each one once it’s acknowledged. The broker is smart (it routes, tracks per-message acks, redelivers on failure); the consumer is relatively dumb (it processes what it’s handed and acks). A consumed-and-acked message is gone — there’s no log to rewind.

Kafka is the mirror image: a dumb broker, smart consumer pull model. Kafka is an append-only log; the broker just stores ordered records and consumers track their own offset and pull at their own pace. Nothing is deleted on consume — a message stays for the retention window, so you can replay it, add a new consumer group that reads from the beginning, or reprocess after a bug fix. RabbitMQ’s per-message ack-and-delete gives you fine-grained workflow control and complex routing; Kafka’s log gives you replay and raw throughput.

The throughput gap is large and structural. RabbitMQ classic queues reach the tens of thousands of messages/sec per queue only in a lightweight, transient config (non-durable, no publisher confirms); turn on durability, persistence, or quorum queues and that drops substantially — often to the low thousands. A single Kafka partition sustains on the order of 1 million messages/sec because it batches sequential writes to a log. The flip side: RabbitMQ delivers with sub-millisecond p50 latency (it pushes as messages arrive), while Kafka’s batching imposes a structural floor of roughly 5–15 ms p50. So the choice isn’t “which is better” — it’s complex per-message routing and low latency (RabbitMQ) versus replayable high-throughput streams (Kafka).

Pick the best fit

You need to deliver order events to 3 services today, expect to add more later, and some only care about EU orders. Pick the exchange topology.

The failure modes that page you at 3am

Three exchange-and-queue mistakes cause most production incidents:

Unbound exchange → silent drops. This is the Hook. By default, basic.publish does not tell the producer whether the message was routed. If no binding matches, the broker drops it and the publish still returns success. The fix is the mandatory flag: set it, and an unroutable message is returned to the producer via a basic.return instead of vanishing — or route a catch-all queue with # so nothing is lost unobserved. Always alert on unroutable counts.

Prefetch (QoS) starvation and memory blowup. RabbitMQ pushes messages, and the prefetch count (basic.qos) caps how many unacknowledged messages a consumer may hold at once. Set it too high — say 1000 — and one slow consumer grabs a huge backlog while fast consumers sit idle, starving them; worse, those unacked messages stay resident in broker memory, and if processing stalls the unacked count climbs unbounded until the node hits its memory watermark and blocks all publishers. The senior default is a low prefetch (start at 1, tune up with metrics), so work spreads fairly and memory stays bounded.

Poison messages with no DLX → infinite redelivery loop. A consumer that nacks-with-requeue on a message it can never process (malformed payload, a bug it always trips) gets the same message back, forever, pinning a worker. The fix is a dead-letter exchange (DLX): configure the queue to dead-letter messages that are rejected or expire, routing them to a separate exchange and quarantine queue you can inspect and reprocess. Without a DLX, a single poison message can stall an entire pipeline.

Durability and HA: classic, mirrored, quorum

A message is only as safe as the queue holding it. Classic queues live on one node; if it dies, the queue’s messages go with it. The old answer was classic mirrored queues (replicas on other nodes), but those were deprecated and removed in RabbitMQ 4.0 — they could confirm a message before it was safely replicated, risking loss on failover. The modern answer is quorum queues: a Raft-based replicated queue that only confirms once a majority of nodes have the message. The tradeoff is concrete — replication across 3 nodes sustains roughly 30K msg/sec for 1KB messages, lower than an unreplicated classic queue, in exchange for not losing data when a node falls over. For anything you can’t afford to lose, that’s the trade a senior takes.

Quiz

A producer publishes to a topic exchange, but a newly added consumer's queue receives nothing while no errors appear anywhere. What's the most likely cause?

Quiz

You need late-arriving consumers to be able to replay the last week of events from the beginning. Which system fits, and why?

Order the steps

Order the path a message takes from producer to consumer in RabbitMQ:

  1. 1 Producer publishes a message with a routing key to a named exchange
  2. 2 The exchange applies its type's rule to the routing key against its bindings
  3. 3 Each matching binding delivers an independent copy to its bound queue
  4. 4 The broker pushes a message to a consumer, up to the prefetch (QoS) limit
  5. 5 The consumer acks; the broker deletes that copy from the queue
Recall before you leave
  1. 01
    A teammate insists RabbitMQ and Kafka are interchangeable message queues. Explain the architectural difference and when each is the right tool.
  2. 02
    Why does a high prefetch count cause both unfairness and outages, and what's the safe default?
Recap

RabbitMQ’s defining move is that producers publish to an exchange, never a queue, and the exchange’s type plus the message’s routing key plus the bindings decide which queues receive an independent copy — possibly none. Direct routes on exact key match, fanout broadcasts to every bound queue, topic matches dotted patterns with * and #, and headers matches header attributes. This is the smart-broker, dumb-consumer push model: the broker routes, pushes, tracks per-message acks, and deletes on consume — the opposite of Kafka’s dumb-broker, smart-consumer pull log that retains messages for replay. The trade is routing richness and sub-millisecond latency at tens of thousands of msg/sec per queue (RabbitMQ) versus replay and roughly a million msg/sec per partition (Kafka). The production failures are predictable: an unbound exchange silently drops messages unless you set the mandatory flag or a catch-all binding; a too-high prefetch starves fast consumers and grows unacked memory until publishers block; a poison message with no dead-letter exchange loops forever. And for durability, classic mirrored queues are gone as of RabbitMQ 4.0 — quorum queues replicate via Raft and confirm only on majority, costing throughput (around 30K msg/sec replicated across three nodes) to stop losing data on failover. Get the exchange type and the bindings right first; everything downstream depends on routing being correct.

Continue the climb ↑RabbitMQ exchanges: multiple-choice review
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.