AMQP’s fanout exchange doesn’t actually broadcast messages; it delivers a copy of each message to every queue bound to it, regardless of routing keys.

Let’s see it in action. Imagine a scenario where a central service publishes an event, and multiple downstream services need to react to it independently. We’ll use RabbitMQ for this.

First, we need to declare a fanout exchange. This exchange doesn’t care about routing keys at all.

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='my_fanout_exchange', exchange_type='fanout')

Next, we’ll declare a few queues. These queues will receive the messages. Crucially, when binding a queue to a fanout exchange, the routing key is ignored.

channel.queue_declare(queue='queue_a')
channel.queue_bind(exchange='my_fanout_exchange', queue='queue_a')

channel.queue_declare(queue='queue_b')
channel.queue_bind(exchange='my_fanout_exchange', queue='queue_b')

channel.queue_declare(queue='queue_c')
channel.queue_bind(exchange='my_fanout_exchange', queue='queue_c')

Now, when we publish a message to my_fanout_exchange, it will be delivered to queue_a, queue_b, and queue_c.

message = "Hello, fanout subscribers!"
channel.basic_publish(exchange='my_fanout_exchange',
                      routing_key='', # Routing key is ignored for fanout
                      body=message)

print(f" [x] Sent '{message}'")
connection.close()

On the consumer side, each service will have its own queue bound to the fanout exchange.

# Consumer for queue_a
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='queue_a') # Ensure queue exists
channel.queue_bind(exchange='my_fanout_exchange', queue='queue_a') # Ensure binding exists

def callback_a(ch, method, properties, body):
    print(f" [x] Received on queue_a: {body.decode()}")

channel.basic_consume(queue='queue_a', on_message_callback=callback_a, auto_ack=True)

print(' [*] Waiting for messages on queue_a. To exit press CTRL+C')
channel.start_consuming()

You would have similar consumers for queue_b and queue_c. When the message is published, all three consumers will independently receive it.

The core problem a fanout exchange solves is decoupling a single message producer from multiple message consumers that all need to receive the exact same message. Think of it like a loudspeaker: one announcement, many listeners. This is distinct from a direct or topic exchange where messages are routed based on specific keys to a subset of consumers. Here, every bound queue is a recipient.

The internal mechanism is quite simple: when a message arrives at a fanout exchange, the exchange simply looks up all the queues that have been bound to it and pushes a copy of the message to each one. There’s no complex routing logic; it’s a one-to-many duplication. The routing_key parameter in basic_publish is effectively ignored by the fanout exchange itself, though the AMQP client might still require it to be present in the method signature.

A subtle but important point is what happens when a consumer disconnects. If a queue is bound to a fanout exchange and its consumer disconnects without acknowledging messages (or if auto_ack is True), those messages are lost for that specific consumer. However, because it’s a fanout exchange, other consumers connected to their own queues bound to the same exchange will still receive their copies. The fanout exchange itself doesn’t maintain message state across consumer disconnections; it’s the queues’ properties (like durability and auto_ack) that govern message persistence and delivery guarantees.

The next step is often dealing with message durability and ensuring that messages aren’t lost if the broker restarts.

Want structured learning?

Take the full Amqp course →