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.