AMQP’s message routing is significantly more powerful than simple queues; it’s a full-blown publish/subscribe system where messages are routed based on headers, not just destinations.

Let’s see this in action with RabbitMQ, a popular AMQP broker. Imagine we have a system that publishes "order" events. Some consumers want all orders, others want only "priority" orders, and yet others want orders from a specific "region."

First, we need to set up our AMQP exchange and bindings. We’ll use a headers exchange type because it allows us to match on arbitrary key-value pairs within message headers.

import pika

# Connect to RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# Declare a headers exchange
channel.exchange_declare(exchange='order_exchange', exchange_type='headers', durable=True)

# Declare queues for different consumers
channel.queue_declare(queue='all_orders_queue', durable=True)
channel.queue_declare(queue='priority_orders_queue', durable=True)
channel.queue_declare(queue='region_us_orders_queue', durable=True)

# Bind queues to the exchange with header matching rules

# Bind 'all_orders_queue' to receive all messages from 'order_exchange'
# This is achieved by specifying a 'x-match' of 'all' and an empty dictionary for headers.
# Technically, any message published to 'order_exchange' will match this if x-match is 'all'.
channel.queue_bind(exchange='order_exchange', queue='all_orders_queue', routing_key='', arguments={'x-match': 'all'})

# Bind 'priority_orders_queue' to receive messages where the 'priority' header is 'high'
# 'x-match': 'all' means ALL specified headers must match.
channel.queue_bind(exchange='order_exchange', queue='priority_orders_queue', routing_key='', arguments={'x-match': 'all', 'priority': 'high'})

# Bind 'region_us_orders_queue' to receive messages where the 'region' header is 'us'
channel.queue_bind(exchange='order_exchange', queue='region_us_orders_queue', routing_key='', arguments={'x-match': 'all', 'region': 'us'})

print("Exchange and queues declared, and bindings set up.")

Now, let’s publish some messages with different headers.

# Publish a general order message
channel.basic_publish(
    exchange='order_exchange',
    routing_key='', # Routing key is ignored by headers exchange
    properties=pika.BasicProperties(
        delivery_mode=2, # Make message persistent
        headers={'type': 'order', 'status': 'new'}
    ),
    body='{"order_id": "12345", "item": "widget", "amount": 10.50}'
)
print("Published general order.")

# Publish a high-priority order message
channel.basic_publish(
    exchange='order_exchange',
    routing_key='',
    properties=pika.BasicProperties(
        delivery_mode=2,
        headers={'type': 'order', 'priority': 'high', 'region': 'eu'}
    ),
    body='{"order_id": "67890", "item": "gadget", "amount": 25.00, "priority": "high"}'
)
print("Published high-priority order.")

# Publish an order specifically for the US region
channel.basic_publish(
    exchange='order_exchange',
    routing_key='',
    properties=pika.BasicProperties(
        delivery_mode=2,
        headers={'type': 'order', 'region': 'us', 'status': 'processing'}
    ),
    body='{"order_id": "13579", "item": "doodad", "amount": 5.75}'
)
print("Published US region order.")

connection.close()

When these messages are published, the order_exchange inspects the headers property of each message.

  • The first message (order_id: 12345) has headers {'type': 'order', 'status': 'new'}. The all_orders_queue binding has x-match: 'all' and no specific headers, so it will receive this message.
  • The second message (order_id: 67890) has headers {'type': 'order', 'priority': 'high', 'region': 'eu'}. The all_orders_queue will receive it. The priority_orders_queue binding has x-match: 'all' and requires priority: 'high'. Since this message has priority: 'high', it will also be routed to priority_orders_queue.
  • The third message (order_id: 13579) has headers {'type': 'order', 'region': 'us', 'status': 'processing'}. The all_orders_queue will receive it. The region_us_orders_queue binding has x-match: 'all' and requires region: 'us'. Since this message has region: 'us', it will be routed to region_us_orders_queue.

Notice that the routing_key parameter in basic_publish and queue_bind is often an empty string for headers exchanges because the routing logic is entirely driven by the headers argument.

The x-match argument in the queue_bind call is crucial. It can be set to 'all' (default) or 'any'.

  • 'all': All the header key-value pairs specified in the binding arguments must exist and match in the message’s headers for the message to be routed.
  • 'any': At least one of the header key-value pairs specified in the binding arguments must exist and match in the message’s headers.

This header-based routing offers immense flexibility, allowing for complex publish/subscribe patterns without needing to manage intricate topic-based routing rules. It’s perfect for scenarios where message content or metadata, rather than a predefined topic structure, dictates delivery.

A common pitfall is forgetting to set x-match explicitly when you intend to use 'any', as 'all' is the default and might lead to unexpected message delivery if you only wanted one condition to match.

The next step in mastering AMQP routing is understanding how to combine different exchange types, like direct and topic, with headers for even more granular control.

Want structured learning?

Take the full Amqp course →