Bindings — The Routing Table
Table of Contents
Bindings — The Routing Table #
A binding is a rule that connects an exchange to a queue (or another exchange). When a message arrives at an exchange, the exchange evaluates its binding table to determine which queues receive the message. The binding table is the routing configuration — changing bindings changes routing behavior without touching producers or consumers.
Creating and Removing Bindings #
# Bind queue 'orders' to exchange 'events' with routing key 'order.created'
channel.queue_bind(
queue='orders',
exchange='events',
routing_key='order.created',
arguments={} # used for headers exchange matching
)
# Remove the binding
channel.queue_unbind(
queue='orders',
exchange='events',
routing_key='order.created',
arguments={}
)
A binding has three components:
- Source exchange: where messages arrive.
- Destination queue (or exchange for E2E bindings): where matched messages go.
- Binding key + arguments: the matching criteria.
Binding Evaluation #
When the broker receives a basic.publish method with routing key R to exchange E, it evaluates E’s binding table:
For each binding B in E's binding table:
if match(B.binding_key, B.arguments, R, message.headers):
deliver message to B.destination
For direct exchanges: match if B.binding_key == R (exact string comparison).
For fanout exchanges: always match (binding_key is ignored).
For topic exchanges: match if the pattern in B.binding_key matches R using word/wildcard rules.
For headers exchanges: match based on B.arguments vs message.headers (the routing key R is ignored).
If no binding matches, the message is discarded — or dead-lettered to the alternate exchange if one is configured.
Direct Binding Matching #
Exchange: 'commands' (direct)
binding: routing_key='create' → queue 'create-handler'
binding: routing_key='delete' → queue 'delete-handler'
binding: routing_key='create' → queue 'audit-log' ← same key, different queue
Message routing_key='create':
→ matches binding 1 → queue 'create-handler'
→ matches binding 3 → queue 'audit-log'
→ does NOT match binding 2 ('delete' ≠ 'create')
Multiple bindings with the same routing key deliver to multiple queues. The same queue can be bound multiple times with different routing keys:
# Same queue bound with multiple routing keys
channel.queue_bind('notifications', 'events', 'user.login')
channel.queue_bind('notifications', 'events', 'user.logout')
channel.queue_bind('notifications', 'events', 'user.registered')
Topic Binding Matching #
The topic exchange implements a pattern-matching algorithm over dot-separated routing keys.
Word segmentation: both the routing key and binding key are split on . into words. Matching is word-by-word:
Binding key: 'orders.*.completed'
Word 1: 'orders' (literal)
Word 2: '*' (match exactly one word)
Word 3: 'completed' (literal)
Routing key: 'orders.europe.completed'
Word 1: 'orders' → matches 'orders' ✓
Word 2: 'europe' → matches '*' (one word) ✓
Word 3: 'completed' → matches 'completed' ✓
→ MATCH
# matching: the # wildcard can match zero or more words, consuming all remaining words in one step.
Binding key: 'orders.#'
Matches: 'orders' (zero additional words)
Matches: 'orders.us' (one additional word)
Matches: 'orders.us.west' (two additional words)
Matches: 'orders.us.west.completed' (three additional words)
Binding key: '#.error'
Matches: 'error' (zero prefix words)
Matches: 'auth.error'
Matches: 'payment.card.error'
Matches: 'a.b.c.d.error'
Edge cases:
- Binding key
#matches every routing key (zero or more words). - Binding key
*matches any single-word routing key only. - Binding key
*.#matches any routing key with at least one word (equivalent to#after the first word). - Routing key
orders(single word) matches bindingorders.#(hash = zero words).
Algorithm: topic matching is a depth-first search through the binding key words. The broker’s implementation is optimized with a trie (prefix tree) structure for O(log N) matching in large binding tables.
Headers Binding Matching #
Headers exchange matching compares the arguments table of the binding against the headers property of the message:
Binding arguments:
'format': 'pdf'
'type': 'monthly'
'x-match': 'all'
Message headers:
'format': 'pdf'
'type': 'monthly'
'department': 'finance'
→ x-match=all: all binding keys present in message headers AND values match → MATCH
x-match rules:
all: every key-value pair in the binding must appear in the message headers with the same value. Extra headers in the message are ignored.any: at least one key-value pair in the binding must appear in the message headers with the same value.
Value types: header values are typed (string, integer, boolean, byte array). The broker compares values of the same type. A binding with count=5 (integer) does not match a message header count='5' (string).
Ignoring x-match key: the x-match binding argument is the control key — it is not matched against message headers. Only the other key-value pairs in the binding arguments are used for matching.
Binding Arguments for Non-Headers Exchanges #
The arguments table in queue_bind is used differently depending on the exchange type:
- direct / topic / fanout:
argumentsis currently unused by RabbitMQ for routing decisions. Future extensions may use it. - headers:
argumentscontains the matching criteria includingx-match.
For exchange-to-exchange bindings on topic exchanges, the routing key pattern in queue_bind works the same way as queue bindings — the binding key is matched against the message’s routing key.
Multiple Bindings and Message Delivery #
A single message can be delivered to multiple queues if multiple bindings match:
Exchange: 'events' (topic)
binding: 'orders.#' → queue 'order-service'
binding: '#.error' → queue 'error-monitor'
binding: 'orders.#' → queue 'audit-log'
Message routing_key='orders.payment.error':
→ matches 'orders.#' → 'order-service'
→ matches '#.error' → 'error-monitor'
→ matches 'orders.#' → 'audit-log'
→ delivered to all three queues (three independent copies)
Each queue receives an independent copy of the message. If one queue’s consumer is slow, it does not affect delivery to the others.
Important: delivery to all matching queues is atomic from the producer’s perspective. If the broker delivers to order-service and audit-log but crashes before delivering to error-monitor, after recovery the message will be re-delivered to error-monitor (assuming durable queues and persistent messages). The broker does not partially deliver.
Binding Lifecycle #
Bindings exist as long as both the exchange and queue exist. When either is deleted, the binding is automatically removed.
Explicit unbind: use queue_unbind to remove a specific binding. This does not affect the queue or exchange — only the routing rule.
Queue delete: deletes all bindings involving that queue from all exchanges.
Exchange delete: deletes all bindings from that exchange to queues.
# Delete exchange (all bindings to this exchange are removed)
channel.exchange_delete(
exchange='old-exchange',
if_unused=True # only delete if no bindings
)
Binding Count and Performance #
The broker evaluates all bindings on every publish. For direct exchanges, this is O(1) via a hash lookup on the routing key. For topic exchanges, the trie structure makes it approximately O(binding_key_length × matching_bindings). For headers exchanges, it is O(bindings × attributes_per_binding).
In practice, even hundreds of bindings on a high-throughput exchange are not a bottleneck — binding evaluation is in-memory and the broker processes millions of messages per second with typical binding tables.
However, extremely large binding tables (tens of thousands of bindings on a single exchange) can impact memory and CPU. For such cases, consider splitting into multiple exchanges with pre-routing, or using alternative routing strategies.
Passive Queue Declare After Bind #
A common initialization pattern: declare queue → declare exchange → bind → start consuming. The ordering matters:
# Correct order: queue first, then exchange, then bind
channel.queue_declare('orders', durable=True)
channel.exchange_declare('events', 'topic', durable=True)
channel.queue_bind('orders', 'events', 'orders.#')
# Start consuming
channel.basic_consume('orders', callback)
If the exchange is declared before the queue, and the producer publishes before the consumer binds, messages are routed to no queue and discarded. The safe pattern is to declare and bind before starting producers.
Summary #
Bindings are routing rules connecting exchanges to queues. Each exchange type uses the binding differently: direct matches exact routing keys, fanout ignores them, topic uses pattern matching, headers matches attribute values. Multiple bindings can match one message — each matching queue receives a copy. Bindings have no independent lifecycle — they are removed when either endpoint is deleted. The binding table is evaluated on every publish; the broker’s trie structure makes topic matching efficient even with many bindings.