Backend / Message queues / RabbitMQ / 02_rabbitmq_exchanges.md

RabbitMQ exchanges and routing

Updated 4 min read source
On this page9
  1. The routing pipeline
  2. The default exchange — the invisible one
  3. Topic wildcards — the exact semantics
  4. The five canonical topologies
  5. Declaring a topology (pika)
  6. Unroutable messages — mandatory flag and alternate exchange
  7. Exchange-to-exchange bindings and other specials
  8. Common pitfalls
  9. Interview angle

RabbitMQ exchanges and routing

How messages find queues. The exchange types are catalogued in RabbitMQ Exchanges, Queues, DLX, and Quorum Queues; this note is about routing behavior and the topologies you build from it — the part interviewers dig into after you’ve named direct/topic/fanout/headers.

The routing pipeline

text
producer --(exchange, routing_key)--> exchange --(bindings match?)--> queue(s) --> consumers

Three facts that drive everything:

  1. Producers never publish to a queue — always to an exchange (possibly the default one).
  2. A binding = (exchange, queue, binding key). Routing is matching the message’s routing key against binding keys — the algorithm depends on exchange type.
  3. If a message matches multiple queues, each gets its own copy; if it matches none, it’s silently dropped (unless mandatory or an alternate exchange is set — below).

The default exchange — the invisible one

The nameless direct exchange ("") auto-binds every queue by its queue name. basic_publish(exchange="", routing_key="tasks") therefore looks like publishing to a queue — it’s still exchange routing. Worth saying explicitly in interviews; “you can publish straight to a queue” is technically never true.

Topic wildcards — the exact semantics

Binding keys on a topic exchange are dot-separated words with two wildcards:

  • * matches exactly one word
  • # matches zero or more words
Routing key logs.* logs.# *.error #
logs.error yes yes yes yes
logs.app.error no yes no yes
logs no yes no yes

Note the classics: logs.* does not match logs (star needs a word) while logs.# does; # alone turns a topic exchange into a fanout.

The five canonical topologies

Pattern Exchange Shape Use
Work queue default/direct 1 queue, N competing consumers background jobs, Celery
Pub/sub fanout 1 exchange → queue per subscriber broadcast invalidation, notifications
Selective routing direct binding key per severity/type route error to pager queue, info to log queue
Topic routing topic wildcard subscriptions orders.eu.*, *.payment.failed
Request/reply (RPC) default + reply_to caller declares exclusive reply queue; correlation_id matches responses sync calls over AMQP (RabbitMQ — Common Interview Questions and Answers Q18)

The key design insight: routing logic lives in the broker topology, not in consumers. Contrast with Kafka, where the broker only knows partitions and any filtering is consumer-side (Kafka vs RabbitMQ — choosing a message broker).

Declaring a topology (pika)

Idempotent declarations — every declare is create-if-missing, so producers and consumers can both declare defensively:

python
import pika

conn = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
ch = conn.channel()

ch.exchange_declare("events", exchange_type="topic", durable=True)
ch.queue_declare("billing.payments", durable=True)
ch.queue_bind("billing.payments", "events", routing_key="payment.*")
ch.queue_bind("billing.payments", "events", routing_key="refund.issued")  # multi-bind is fine

ch.basic_publish(
    exchange="events",
    routing_key="payment.captured",
    body=b'{"order_id": 42}',
    properties=pika.BasicProperties(delivery_mode=pika.DeliveryMode.Persistent),
)

Re-declaring with different properties (durable, type, arguments) raises a channel error — topology changes in production usually mean new names + migration, not in-place edits.

Unroutable messages — mandatory flag and alternate exchange

Two safety nets for “matched no queue”:

  • mandatory=True on publish: broker returns the message to the producer (async basic.return callback) instead of dropping. Easy to publish-and-forget past it; you must register the return handler.
  • Alternate exchange (AE): exchange_declare(..., arguments={"alternate-exchange": "unrouted"}) — anything unroutable is diverted to the AE (typically a fanout into an audit queue). Set-and-forget, preferred in production over per-publish flags.

Same idea at the consumer end is the DLX — dead-lettering on reject/TTL/overflow (RabbitMQ Exchanges, Queues, DLX, and Quorum Queues); AE catches routing misses, DLX catches consumption failures.

Exchange-to-exchange bindings and other specials

  • E2E bindings (exchange_bind): compose routing in layers — e.g., one events topic exchange feeding per-team fanouts. Keeps one producer-facing exchange while teams own their sub-topologies.
  • Consistent-hash exchange (plugin): shards messages across N queues by hashing the routing key — RabbitMQ’s answer to partition-style parallelism with per-key ordering (each key always lands in the same queue).
  • Delayed message exchange (plugin): schedule delivery with x-delay — the standard way to do retry-with-backoff topologies (retry queue with TTL + DLX back to the work queue is the plugin-free alternative).
  • Headers exchange: match on message headers with x-match: all|any instead of routing key — rare; reach for it only when routing criteria genuinely aren’t a single string.

Common pitfalls

  • Publishing before any queue is bound → message dropped, no error, “RabbitMQ is losing messages.” Declare bindings before producing, or use AE/mandatory.
  • Fanout used where topic was needed → every consumer parses and discards 95% of traffic that the broker could have filtered.
  • One binding per event type on a direct exchange scaling into hundreds of bindings — that’s the sign you wanted topic with a key convention (domain.entity.action).
  • Treating routing keys as free-form strings — settle a convention early; wildcards are only as good as the key schema.

Interview angle 4

  • “Walk a message from producer to consumer.” — Exchange → binding match → copies to queues → competing consumers per queue. Bonus points for the default exchange detail.
  • “Difference between * and # in topic bindings?” — One word vs zero-or-more; give the logs.* vs logs.# edge case.
  • “What happens to a message that matches no binding?” — Dropped silently; mitigate with alternate exchange or mandatory. This distinguishes people who ran it in production.
  • “How do you get per-key ordering with parallel consumers?” — Consistent-hash exchange (or one queue per key group) — and compare with Kafka’s partition-key model.