What are RabbitMQ's exchange types — direct, topic, fanout, headers — and when do you use each?
RabbitMQ routes every message through an exchange, and the exchange type — direct, topic, fanout, or headers — decides which queues actually receive it.
LEARN BY DOINGPut your intuition to work3 steps
What are RabbitMQ's exchange types — direct, topic, fanout, headers — and when do you use each?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.In RabbitMQ, a producer never publishes directly into a queue — it publishes a message to an exchange, and the exchange decides which queue (or queues) actually receive a copy, based on bindings. A binding connects an exchange to a queue, optionally with a routing key or header pattern. This indirection is what makes RabbitMQ's routing flexible enough to model direct handoffs, hierarchical topics, broadcasts, and multi-attribute filters — all through the same core primitive.
#The four exchange types
| Exchange type | Routing logic | Typical use case |
|---|---|---|
| direct | Exact match between the message's routing key and a queue's binding key | Route to a specific known consumer type (e.g. email, sms) |
| topic | Pattern match on a dot-separated routing key using * (one word) and # (zero or more words) | Hierarchical routing, e.g. log severity + service name |
| fanout | Ignores the routing key entirely — delivers to every bound queue | Broadcast to all subscribers, e.g. cache invalidation |
| headers | Match on message header key/value pairs instead of the routing key | Routing on multiple independent attributes that don't fit a single string |
#Direct exchange
A direct exchange is the simplest: a message with routing key email goes to every queue bound with binding key email, and nowhere else. It behaves like a dictionary lookup — useful when you have a small, known set of message categories and want each one handled by a dedicated queue/worker pool.
import pikaconnection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))channel = connection.channel()channel.exchange_declare(exchange='notifications.direct', exchange_type='direct')channel.queue_declare(queue='email_queue')channel.queue_declare(queue='sms_queue')channel.queue_bind(exchange='notifications.direct', queue='email_queue', routing_key='email')channel.queue_bind(exchange='notifications.direct', queue='sms_queue', routing_key='sms')channel.basic_publish(exchange='notifications.direct',routing_key='email', # delivered only to email_queuebody='{"to": "user@example.com", "template": "welcome"}')
The default exchange (the nameless "" exchange every RabbitMQ vhost has) is a special case of direct routing where the routing key is treated as a queue name directly — that's why basic_publish(exchange='', routing_key='my_queue', ...) looks like it's "publishing straight to a queue," even though it's still going through an exchange.
#Topic exchange
A topic exchange generalizes direct routing with wildcard matching over a dot-separated routing key. * matches exactly one word in that position; # matches zero or more words. This is the exchange type to reach for whenever routing keys are naturally hierarchical.
channel.exchange_declare(exchange='logs.topic', exchange_type='topic')channel.queue_declare(queue='payment_errors')channel.queue_declare(queue='all_logs')channel.queue_bind(exchange='logs.topic', queue='payment_errors', routing_key='logs.error.*')channel.queue_bind(exchange='logs.topic', queue='all_logs', routing_key='logs.#')channel.basic_publish(exchange='logs.topic',routing_key='logs.error.payment-service', # matches BOTH bindings abovebody='Payment gateway timeout')
A single publish can match multiple bindings — here, both payment_errors and all_logs receive a copy, because both patterns match logs.error.payment-service.
#Fanout exchange
A fanout exchange ignores the routing key completely and delivers every message to every queue bound to it. It's the RabbitMQ equivalent of a broadcast — useful for pub/sub scenarios where every subscriber needs its own copy of every event, like invalidating an in-memory cache across every running instance of a service.
channel.exchange_declare(exchange='cache.invalidate', exchange_type='fanout')for instance_queue in ['api_instance_1', 'api_instance_2', 'api_instance_3']:channel.queue_declare(queue=instance_queue, exclusive=True)channel.queue_bind(exchange='cache.invalidate', queue=instance_queue) # no routing key neededchannel.basic_publish(exchange='cache.invalidate', routing_key='', body='invalidate:user:42')
Note each instance binds its own exclusive, typically auto-generated queue — that's what turns a fanout into "every listener gets a copy" instead of the round-robin, competing-consumers pattern you'd get if all instances shared one queue.
#Headers exchange
A headers exchange routes on message header attributes instead of the routing key, using an x-match argument of all (every listed header must match) or any (at least one must match). It's the least commonly used type, reserved for cases where routing depends on multiple independent attributes that don't compress cleanly into one routing-key string.
channel.exchange_declare(exchange='orders.headers', exchange_type='headers')channel.queue_declare(queue='high_value_us_orders')channel.queue_bind(exchange='orders.headers',queue='high_value_us_orders',arguments={'x-match': 'all', 'country': 'US', 'tier': 'high-value'})channel.basic_publish(exchange='orders.headers',routing_key='', # ignored by headers exchangesbody='{"orderId": 981, "total": 4200}',properties=pika.BasicProperties(headers={'country': 'US', 'tier': 'high-value'}))