backend / message queues / rabbitmq / 02_rabbitmq_exchanges.md

RabbitMQ exchanges and routing

4 min read source

RabbitMQ exchanges and routing

How messages find queues. The exchange types are catalogued in 03_exchanges_queues_dlx.md; 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

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 (04_rabbitmq_interview.md 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 (../00_broker_comparison.md).

Declaring a topology (pika)

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

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 (03_exchanges_queues_dlx.md); 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

  • “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.