3 AMQP Routing Topology Contracts
Start with the story: A producer should not need to know every team that wants a copy of a message. AMQP topology makes that a broker decision: exchanges inspect the routing key, bindings state who qualifies, and queues hold each matched copy.
3.1 Learning Objectives
After this page, you should be able to:
- Explain why AMQP producers publish to exchanges instead of addressing queues directly.
- Trace the producer -> exchange -> binding -> queue -> consumer path for a routed message.
- Choose direct, topic, fanout, or headers exchanges from the matching rule each one applies.
- Explain how the default exchange makes tutorials look like they publish directly to queues.
- Separate fan-out copies from competing-consumer work sharing in a queue topology.
3.2 Why This Follows AMQP Core Concepts
AMQP Core Concepts introduces exchanges, queues, bindings, routing keys, exchange types, and consumer patterns. This page tightens the topology contract underneath those terms: a producer sends one message to an exchange, bindings decide which queues get copies, and queue layout decides whether consumers share work or each receive their own event stream.
Use it when a queue is unexpectedly empty, a broadcast workload is accidentally processed by only one service, workers are multiplying event copies, or a RabbitMQ topology review needs to prove that routing semantics match the intended system behavior.
3.3 Routing Path Contract
The single idea that separates AMQP 0-9-1 (the model RabbitMQ implements) from a simpler bus is that a producer never names a queue. It publishes to an exchange and attaches a routing key – a short string such as sensor.line1.temperature. The exchange holds no messages of its own; it looks at every binding that links it to a queue and copies the message into each queue whose binding key matches the routing key.
So the delivery path always has five named parts: producer -> exchange -> binding -> queue -> consumer. Change the bindings and you re-wire the whole system without touching a line of producer code. That indirection is exactly what MQTT’s topic tree gives up in exchange for simplicity.
Worked example: a temperature device publishes once to exchange iot.events with routing key plant1.line3.temperature. One queue bound with plant1.# receives a site-wide copy, another bound with *.line3.* receives the line-3 copy, and a third bound with #.vibration receives nothing. The producer did not know those queue names and did not send three messages; the exchange evaluated three bindings and made two copies. If operations later adds a billing queue bound with plant1.line3.#, the producer still publishes the same packet, but the topology now creates a third copy.
That makes troubleshooting concrete. When a queue is empty, inspect the exchange name, routing key, and binding keys before blaming the producer. A perfectly published message can still be unrouted if no binding matches, or duplicated if two bindings match the same queue path through different exchanges.
If you only need the intuition: the exchange is a sorting rule, the binding key is the label on a mailbox slot, and the routing key is the address written on the envelope. A message drops into every slot whose label matches.
3.4 Exchange Type Matching Rules
The four standard exchange types differ only in how they compare a routing key to a binding key. Choosing one is choosing a matching rule, not a feature set.
A useful rule of thumb: choose the narrowest matching rule that still lets operations add subscribers without producer changes. Direct is easiest to audit, fanout is easiest to reason about for broadcast, topic is the usual IoT default when keys carry hierarchy, and headers only pays off when routing depends on several independent attributes.
| Exchange type | Matching rule | Use it when |
|---|---|---|
direct |
Binding key must equal the routing key exactly. | Fixed categories, e.g. route alarm to one queue and telemetry to another. |
topic |
Dot-separated words; * matches exactly one word, # matches zero or more words. |
Hierarchical keys like site.floor.device.metric where subscribers want slices. |
fanout |
Routing key ignored; copied to every bound queue. | Broadcast, e.g. a config change every service must see. |
headers |
Matches on message header attributes, with x-match=all or x-match=any, instead of the routing key. |
Routing on several typed properties at once (region + priority). |
There is also a pre-declared nameless default exchange (""). Every queue is automatically bound to it with a binding key equal to the queue’s own name, so publishing to "" with routing key orders lands straight in the queue named orders. That is why beginner tutorials look like they publish “directly to a queue” – they are quietly using the default direct exchange.
Worked example – telemetry with a topic exchange. Sensors publish to exchange iot.telemetry with routing keys shaped site.floor.metric:
- Data-lake queue binds
#-> receives everything. - Temperature-dashboard queue binds
*.*.temperature-> matcheshq.floor1.temperaturebut nothq.floor1.vibration. - HQ-only audit queue binds
hq.#-> matches any depth of key that starts withhq.
A single publish of hq.floor1.temperature is copied into all three queues, because a topic exchange delivers to every matching binding, not the most specific one.
3.5 Queue Copy Semantics
Once messages reach queues, how many copies exist is decided by queue topology, not by the exchange. Two patterns are constantly confused:
- Competing consumers (work queue): several consumers subscribe to the same queue. Each message goes to exactly one of them, dispatched by
basic.qosprefetch so a busy worker is not overloaded. This scales throughput and is how you parallelise processing. - Fan-out (pub/sub): each consumer declares its own queue and binds it to the exchange. Every consumer’s queue gets its own copy, so all consumers see every message. This scales distribution, not throughput.
The same exchange can feed both at once: a fanout exchange with three bound queues, where one of those queues has ten competing consumers behind it. Getting this wrong is the classic bug where “adding more consumers” either fails to speed anything up (they were all on separate fanned-out queues) or silently drops broadcast semantics (they were all sharing one queue).
Numbers make the difference visible. Suppose five analytics services must each see every telemetry event, and each service runs three worker processes. The correct topology has five queues bound to the exchange, one per service, and three competing consumers attached to each queue. A single telemetry event becomes five queue copies, one for each service; within a service, only one of its three workers receives that copy. If all 15 workers shared one queue, the event would be processed once total and four services would miss it. If every worker had its own fanned-out queue, the event would be processed 15 times, tripling the work inside each service. Queue topology, not worker count alone, controls the semantics.
Prefetch then shapes the backlog behind each choice. With three workers on one service queue and prefetch_count=10, that service can have up to 30 unacknowledged messages in flight while the other four service queues still keep their independent copies. Scaling a service is therefore adding workers to its queue; adding a new service is binding a new queue.