Exchanges — How Messages Are Routed
Table of Contents
Exchanges — How Messages Are Routed #
An exchange receives messages from producers and routes them to queues. It is a stateless routing agent — it holds no messages. The routing decision is made by evaluating the message’s routing key (or headers) against the exchange’s binding table. An exchange with no matching bindings silently discards the message (unless mandatory=True is set).
Declaring an Exchange #
channel.exchange_declare(
exchange='orders',
exchange_type='direct',
durable=True, # survives broker restart
auto_delete=False, # not deleted when last binding removed
internal=False, # reachable by producers (not exchange-to-exchange only)
arguments={} # optional arguments (e.g., alternate-exchange)
)
Parameters:
durable: if True, the exchange survives broker restart. Non-durable exchanges are lost on restart.auto_delete: if True, the exchange is deleted when all its bindings are removed. Useful for temporary exchanges.internal: if True, the exchange cannot be published to by clients — only by other exchanges (used in exchange-to-exchange routing).passive: if True, do not create — check existence. Returns 404 if not found. Useful for verifying topology.arguments: a table of optional arguments. Supported by RabbitMQ:alternate-exchange(see below).
Idempotent declaration: declaring an exchange that already exists with identical parameters is a no-op (success). Declaring with different parameters raises PRECONDITION_FAILED (channel close 406).
The Four Exchange Types #
Direct Exchange #
A direct exchange routes messages to queues whose binding key exactly matches the message’s routing key.
Exchange: 'commands' (type=direct)
binding: routing_key='login' → queue 'session-service'
binding: routing_key='logout' → queue 'session-service'
binding: routing_key='login' → queue 'audit-log' ← same key, multiple queues
Message published with routing_key='login':
→ delivered to 'session-service' AND 'audit-log'
Message published with routing_key='register':
→ no matching binding → discarded (or returned if mandatory=True)
Multiple queues can be bound with the same routing key — the message is delivered to all of them (fan-out behavior for that specific key). One queue can be bound multiple times with different routing keys.
Use case: event type routing. Each event type (login, logout, purchase, refund) has a dedicated routing key. Services bind to the keys they care about.
Fanout Exchange #
A fanout exchange routes messages to all bound queues, ignoring the routing key entirely.
Exchange: 'notifications' (type=fanout)
binding: → queue 'email-service'
binding: → queue 'sms-service'
binding: → queue 'push-service'
Any message published to 'notifications':
→ delivered to all three queues
The routing key in basic.publish is ignored for fanout exchanges. Any value (including empty string) routes to all bound queues.
Use case: broadcast. Cache invalidation, system-wide notifications, real-time dashboards that need the same event delivered to many consumers. Every consumer that needs the event creates its own queue and binds it to the fanout exchange.
Topic Exchange #
A topic exchange routes messages to queues whose binding key pattern matches the message’s routing key. The routing key is a dot-separated string of words (e.g., orders.europe.completed). Binding keys can contain wildcards:
*(star): matches exactly one word.#(hash): matches zero or more words.
Exchange: 'events' (type=topic)
binding: 'orders.*.completed' → queue 'order-completions'
binding: 'orders.europe.#' → queue 'europe-auditor'
binding: '#.error' → queue 'error-log'
binding: 'orders.#' → queue 'order-all'
Message routing_key='orders.europe.completed':
→ matches 'orders.*.completed' (star=europe) → 'order-completions'
→ matches 'orders.europe.#' (hash=completed) → 'europe-auditor'
→ does NOT match '#.error' (last word=completed, not error)
→ matches 'orders.#' (hash=europe.completed) → 'order-all'
Message routing_key='payments.us.error':
→ matches '#.error' (hash=payments.us) → 'error-log'
→ does NOT match any orders.* pattern
Wildcard semantics:
*matches exactly one word between dots.orders.*.completedmatchesorders.us.completedbut notorders.completedororders.us.west.completed.#matches zero or more words.orders.#matchesorders,orders.us,orders.us.west,orders.us.west.completed.- A routing key that is a single word (no dots) can match
#(zero or more words from a binding of just#).
Use case: multi-criteria routing. Log levels (*.error, *.warning) combined with subsystems (auth.*, payment.*). IoT device events by region and device type.
Headers Exchange #
A headers exchange routes messages based on header attribute values rather than the routing key. The binding specifies key-value pairs; the exchange matches against the message’s headers property.
Exchange: 'reports' (type=headers)
binding: {format: 'pdf', type: 'monthly', x-match: 'all'} → queue 'pdf-monthly-reports'
binding: {format: 'csv', x-match: 'any'} → queue 'csv-reports'
Message headers: {format: 'pdf', type: 'monthly', department: 'finance'}
→ matches binding 1 (all: format=pdf AND type=monthly ✓) → 'pdf-monthly-reports'
→ does NOT match binding 2 (any: format=csv ✗, no csv key)
Message headers: {format: 'csv', department: 'hr'}
→ does NOT match binding 1 (all: format≠pdf)
→ matches binding 2 (any: format=csv ✓) → 'csv-reports'
x-match argument:
all: all binding key-value pairs must match the message headers (AND logic).any: at least one binding key-value pair must match (OR logic).
The routing key in basic.publish is ignored for headers exchanges.
Use case: complex multi-attribute filtering. Headers exchanges are more expressive than topic exchanges for matching on multiple independent dimensions, but they are slower (O(attributes) matching vs O(routing_key length)). Rarely used in practice — topic exchanges handle most multi-criteria routing needs more simply.
Exchange Properties #
Durable vs Transient #
Durable exchanges survive broker restarts. They are persisted to disk. On restart, the broker recreates them. Use durable exchanges for production workloads.
Transient (non-durable) exchanges exist only in memory and are lost on restart. They are faster (no disk write on declare) but useless after a broker restart. Only useful for short-lived topologies in test environments.
Note: exchange durability and message durability are independent properties. A durable exchange does not make messages durable — messages must be declared persistent (delivery-mode=2) to survive restart. A non-durable exchange still delivers messages until the restart.
Auto-Delete #
An auto-delete exchange is deleted automatically when its last binding is removed. Useful for temporary routing topologies created per-request:
# Create a per-session fanout exchange, auto-deleted when session ends
channel.exchange_declare(
exchange=f'session.{session_id}',
exchange_type='fanout',
auto_delete=True
)
channel.queue_bind(queue='session-handler', exchange=f'session.{session_id}')
# When the binding is removed (queue deleted/unbound), exchange is auto-deleted
Internal #
An internal exchange cannot be published to by client connections — only by other exchanges via exchange-to-exchange bindings. Used as an intermediate routing step in complex topologies.
channel.exchange_declare(
exchange='internal-router',
exchange_type='topic',
internal=True # clients cannot publish here directly
)
Exchange-to-Exchange Bindings #
RabbitMQ supports binding an exchange to another exchange (not a standard AMQP 0-9-1 feature, but a widely-used RabbitMQ extension):
# Bind exchange 'logs.europe' to exchange 'logs.all'
channel.exchange_bind(
destination='logs.all', # receives from source
source='logs.europe', # messages published here flow to destination
routing_key='#'
)
Messages published to logs.europe are routed by logs.europe’s binding table, then any messages routed to logs.all are further routed by logs.all’s binding table. This creates a multi-stage routing pipeline:
Producer → 'logs.europe' (topic exchange)
[routing_key='auth.error'] → 'logs.all' (via exchange binding)
→ queue 'all-error-log' (if 'logs.all' has matching binding)
[routing_key='payment.info'] → not bound to 'logs.all' → discarded
Use case: hierarchical routing. A regional exchange logs.europe captures all European logs; binding it to a global logs.all exchange aggregates them. Internal exchanges and exchange-to-exchange bindings enable routing graphs without requiring producers to know the full topology.
The Alternate Exchange #
If a message is published to an exchange and no binding matches, it is normally discarded (or returned to the producer if mandatory=True). The alternate exchange is a fallback: unroutable messages are forwarded to the alternate exchange instead of being discarded.
channel.exchange_declare(
exchange='orders',
exchange_type='direct',
arguments={'alternate-exchange': 'unrouted-messages'}
)
channel.exchange_declare(
exchange='unrouted-messages',
exchange_type='fanout'
)
channel.queue_declare('dead-letters')
channel.queue_bind(queue='dead-letters', exchange='unrouted-messages')
Messages published to orders that match no binding are forwarded to unrouted-messages (fanout), which delivers them to dead-letters. This provides visibility into routing misconfiguration without losing messages or requiring mandatory=True on every publish.
Pre-Declared Exchanges #
Every AMQP 0-9-1 broker pre-declares several exchanges that cannot be deleted:
| Name | Type | Purpose |
|---|---|---|
"" (empty string) | direct | Default exchange — routes to queue by name |
amq.direct | direct | Standard direct exchange |
amq.fanout | fanout | Standard fanout exchange |
amq.topic | topic | Standard topic exchange |
amq.headers | headers | Standard headers exchange |
amq.match | headers | Alias for amq.headers |
The pre-declared exchanges exist in every vhost. They cannot be deleted or redeclared with different properties.
Routing Key Design #
Direct exchange routing keys: event types in past tense (order.created, payment.charged, user.registered). Short, opaque strings.
Topic exchange routing keys: hierarchical, most-specific-first or domain-first. Two common conventions:
Convention 1 (domain.subdomain.event):
payments.card.charged
payments.bank.transferred
orders.items.reserved
orders.fulfillment.shipped
Convention 2 (event.domain.context):
created.order.us
updated.order.europe
error.payment.card
Recommendation: use domain.subdomain.event (orders.europe.completed) — it allows orders.# to capture all order events, orders.europe.* for European orders only, and *.*.completed for all completions across domains.
Avoid: routing keys that are UUIDs, integers, or opaque identifiers — these cannot be pattern-matched and force direct exchange semantics.
Summary #
Exchanges are the routing layer of AMQP. The four types cover point-to-point (direct), broadcast (fanout), pattern routing (topic), and attribute routing (headers). Exchange properties — durable, auto-delete, internal — control lifecycle and access. Exchange-to-exchange bindings enable routing pipelines. The alternate exchange catches unroutable messages. Routing key design for topic exchanges determines what filtering topologies consumers can express without producer changes.