RabbitMQ exchanges and bindings: which queue does a message reach? Basic
Estimated reading time: 8 min read
The problem
Section titled “The problem”The order service publishes order.paid. Billing must receive it to record an invoice; Audit must also receive it to keep a trail. Two naive approaches both fail:
- Two consumers listening on one queue: each message is normally processed by one consumer, not both.
- The producer sends twice, into two queues: it must know every service’s queue name and change code whenever a new subscriber appears.
RabbitMQ solves routing with exchanges and bindings: the producer sends to an exchange, and the broker decides which queues receive it based on declared rules.
But an exchange is not a message store. If no queue matches, a publish may reach no queue at all. “The publish did not throw” does not mean “a consumer received it”; there may not even be a queue holding the message.
This article follows order.paid into two queues and compares the four exchange types common in AMQP 0-9-1.
Core concepts
Section titled “Core concepts”- A producer publishes a message to an exchange with a routing key (a string such as
order.paid). - A binding is a relationship between an exchange and a queue, with a matching rule.
- A queue holds messages for consumers; the queue type and durability determine other storage properties.
An exchange can route the same message to many queues; each queue then has its own copy for its consumers. Multiple consumers on the same queue share work instead; they do not fan out to every consumer.
Loading diagram…
With a direct exchange, the binding key must equal the routing key exactly. For example, binding order.paid receives order.paid, not order.created. Multiple queues with the same binding key all receive it; direct does not mean “only one queue”.
With a topic exchange, the routing key consists of dot-separated segments. * matches exactly one segment; # matches zero or more segments. Binding order.* matches order.paid but not order.payment.failed; order.# matches both.
With a fanout exchange, the routing key is ignored: every queue bound to the exchange gets a copy. Good for simple broadcast, but it does not filter by event type.
With a headers exchange, routing is based on message headers and binding arguments such as x-match=all or any, instead of only the routing key.
It can be useful when the condition does not fit neatly into a key, but configuration and observability are harder; for ordinary event types, topic/direct are usually easier to understand.
The default exchange is a special direct exchange with the empty name ""; every queue is automatically bound to it by queue name. So the sendToQueue('billing', ...) API actually publishes through the default exchange.
It is convenient for simple work queues, but if the producer needs to be decoupled from consumers’ queue names, create a dedicated exchange with explicit bindings.
| Exchange | Routing condition | Which queue does order.paid reach? |
|---|---|---|
| Direct | Key exactly equals the binding key | Queues bound with order.paid |
| Topic | Segment pattern with *, # |
Queues bound with order.paid, order.*, order.# |
| Fanout | Key ignored | Every bound queue |
| Headers | Headers matched per arguments | Queues with a matching header binding |
Topic exchange: key and pattern match table
Section titled “Topic exchange: key and pattern match table”A routing key is a list of dot-separated words (up to 255 bytes). * substitutes exactly one word; # substitutes zero or more words. A queue matching through several bindings still receives the message once.
| Routing key | order.paid |
order.* |
order.# |
*.paid |
#.failed |
|---|---|---|---|---|---|
order.paid |
match | match | match | match | no |
order.created |
no | match | match | no | no |
order.payment.failed |
no | no | match | no | match |
order |
no | no | match | no | no |
payment.paid |
no | no | no | match | no |
Two special cases: a lone # binding receives every message (like fanout), and a binding without wildcards behaves like direct. A message that matches no binding is dropped.
Default to a topic exchange for business events: it covers direct and fanout, and adding subscribers requires no producer changes. Use fanout only for true broadcast, and headers when the condition cannot be expressed as a key.
How it works
Section titled “How it works”Before publishing, the application declares the topology: exchange orders, queue billing, queue audit, binding billing with order.paid, binding audit with order.#.
- Manage topology with versioned code/config/deployment; do not let an environment be configured by hand in the UI and then forgotten.
- Grant the right
configure,write,readpermissions per service; “the producer can publish” does not mean it may create any queue it likes.
The producer sends order.paid. The topic exchange compares the routing key with each binding: order.paid exactly matches billing’s binding; audit’s order.# matches too. The broker places the message in both queues.
When the billing consumer acks its delivery from the billing queue, the message in the audit queue does not disappear. The two queues have their own backlog, retries, consumer count and dead-letter policy.
| Published routing key | billing (order.paid) |
audit (order.#) |
|---|---|---|
order.paid |
receives | receives |
order.created |
no | receives |
payment.paid |
no | no (unless the topology has other queues) |
By default, unroutable messages may be dropped. Three related mechanisms, none replacing the others:
mandatoryflag (AMQP): the broker sendsbasic.returnto the publisher when the message cannot be routed.- Publisher confirm: confirms the broker has taken responsibility for the publish within its scope; it does not by itself prove that a queue matched.
- Alternate exchange: handles unroutable messages per policy.
For important events, test the no-binding case, monitor returns and audit the topology.
A durable exchange/queue and a persistent message are separate settings. A persistent message in a transient queue does not become durable across restarts. Conversely, a durable queue does not mean every message sent to it is persistent.
Quorum queues have their own replication and confirm rules. The demo only uses temporary queues to avoid leaving local data behind; it is not a production configuration template.
Real-world example
Section titled “Real-world example”Run the demo against a local RabbitMQ:
docker compose -f examples/docker-compose.yml up -d rabbitmqnpx --prefix examples ts-node examples/src/rabbitmq-routing/routing-demo.tsThe demo creates a topic exchange and two temporary exclusive queues, binds them as in the diagram, and publishes event-42 with key order.paid. Result on rabbitmq:4-management-alpine: billing: event-42 and audit: event-42.
Temporary queues disappear when the connection closes, so the demo says nothing about durability. It uses exclusive: true because transient non-exclusive queues are deprecated and, since RabbitMQ 4.3.0, cannot be declared by default (deprecated_features.permit.transient_nonexcl_queues).
Try changing billing’s binding to order.created: billing prints missing, audit still receives it. Change the published key to payment.paid: both print missing. The demo uses basic.get for brevity; real applications use consumer subscriptions, manual ack and prefetch.
Checklist
Section titled “Checklist”- Each consuming service has its own queue bound to the exchange; never let two services share one queue.
- Business events default to a topic exchange; fanout only for true broadcast, headers when the condition cannot be expressed as a key.
- Topology (exchanges, queues, bindings) is managed by versioned code/config;
configure,write,readpermissions are granted correctly per service. - Important events are published with
mandatoryor have an alternate exchange, and returned/unroutable counts are monitored. - Test routing keys at the edges (
*exactly one segment,#zero or more) and the case where no binding matches. - Configure durability on both sides:
durablequeues andpersistentmessages. - Rename routing keys in compatible phases; never change the key on one side before the bindings are updated.
Trade-offs
Section titled “Trade-offs”Routing through exchanges
- The producer does not need to know each consumer’s queue name.
- One event can reach many queues, and each service keeps its own progress.
- Direct/topic/fanout/headers offer different levels of filtering.
Design cost
- Topology and routing keys are a contract that needs versioning/testing.
- Unroutable messages can be dropped without mandatory/alternate exchange and monitoring.
- Many broad bindings (
#) can increase fan-out and storage/consumption cost beyond expectations.
Common mistakes
Section titled “Common mistakes”Summary
Section titled “Summary”Self-check: (1) Do two consumers on one queue both see order.paid? (2) Does order.* match order.payment.failed? (3) Does a publisher confirm prove that a queue matched?