RabbitMQ is a message broker that implements the AMQP (Advanced Message Queuing Protocol). At the heart of RabbitMQ’s routing model lies the exchange. Exchanges are responsible for receiving messages from producers and routing them to one or more queues based on defined rules.
Choosing the correct exchange type is critical for building scalable, correct, and maintainable systems—especially in real-time systems such as chat, notifications, and event-driven architectures.
This article explains the three most commonly used exchange types—Direct, Fanout, and Topic—and demonstrates how each routes messages, when to use them, and common mistakes to avoid.
Core Concepts Recap
Before diving into exchange types, it’s important to understand a few fundamental components:
Producer: Sends messages to an exchange.
Exchange: Routes messages to queues based on rules.
Queue: Stores messages until they are consumed.
Binding: A rule that links a queue to an exchange.
Routing Key: A string attached to a message, used by the exchange to decide where the message goes.
An exchange never stores messages. It only decides where messages should be routed.
1. Direct Exchange
How Direct Exchange Works
A direct exchange routes messages to queues only if the routing key exactly matches the binding key.
- Routing is deterministic
- Matching is strict equality
- No pattern matching is performed
Let's take at a PHP/Laravel example:
Install the client:
composer require php-amqplib/php-amqplib
Example .env
Q_HOST=127.0.0.1
MQ_PORT=5672
MQ_USER=guest
MQ_PASS=guest
MQ_VHOST=/
Typical connection factory (recommended to reuse in services):
use PhpAmqpLib\Connection\AMQPStreamConnection;
embed function mqConnection(): AMQPStreamConnection {
return new AMQPStreamConnection(
env('MQ_HOST'),
env('MQ_PORT'),
env('MQ_USER'),
env('MQ_PASS'),
env('MQ_VHOST')
);
}
Publish to a Direct Exchange
use PhpAmqpLib\Message\AMQPMessage;
public function publishOrderCreated(array $payload)
{
$connection = mqConnection();
$channel = $connection->channel();
$exchange = 'orders.direct';
$queue = 'orders.created.queue';
$key = 'order.created';
// Topology
$channel->exchange_declare($exchange, 'direct', false, true, false);
$channel->queue_declare($queue, false, true, false, false);
$channel->queue_bind($queue, $exchange, $key);
$msg = new AMQPMessage(json_encode($payload), [
'content_type' => 'application/json',
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);
$channel->basic_publish($msg, $exchange, $key);
$channel->close();
$connection->close();
}
Consumer for a Direct Queue (manual ack)
$channel->basic_qos(null, 1, null);
$channel->basic_consume('orders.created.queue', '', false, false, false, false,
function ($msg) {
$data = json_decode($msg->body, true);
try {
// process...
$msg->ack();
} catch (\Throwable $e) {
$msg->nack(false, true); // requeue on failure
}
}
);
while ($channel->is_consuming()) {
$channel->wait();
}
When to Use Direct Exchange
Task queues (background jobs)
Worker pools
- Point-to-point messaging
- Routing messages to a specific consumer group
Characteristics
✔ Predictable routing
✔ High performance
❌ Not suitable for broadcast
❌ No pattern matching
2. Fanout Exchange
How Fanout Exchange Works
A fanout exchange routes messages to all bound queues, ignoring the routing key entirely.
- Routing key is irrelevant
- Every bound queue receives a copy
- Pure broadcast semantics
Visual Explanation
In the diagram’s Fanout Exchange section:
- The producer sends a message
- The fanout exchange delivers the message to Queue X, Queue Y, and Queue Z
- All consumers receive the same message independently
When to Use (Chat Example)
Fanout is perfect when multiple servers must receive the same message, such as a chat system where multiple WebSocket nodes need to forward messages to connected clients.
Example:
- ws.node1.queue
- ws.node2.queue
- ws.node3.queue
Every node receives the message and delivers it to clients connected to that node.
Laravel Example: Publish to Fanout (Chat Broadcast)
use PhpAmqpLib\Message\AMQPMessage;
public function broadcastChatMessage(array $payload)
{
$connection = mqConnection();
$channel = $connection->channel();
$exchange = 'chat.fanout';
$channel->exchange_declare($exchange, 'fanout', false, true, false);
$msg = new AMQPMessage(json_encode($payload), [
'content_type' => 'application/json',
]);
// routing key is ignored for fanout; pass '' conventionally
$channel->basic_publish($msg, $exchange, '');
$channel->close();
$connection->close();
}
Fanout Consumers: Each server has its own queue
On WebSocket Server 1:
$exchange = 'chat.fanout';
$queue = 'ws.server1.queue';
$channel->exchange_declare($exchange, 'fanout', false, true, false);
$channel->queue_declare($queue, false, true, false, false);
$channel->queue_bind($queue, $exchange);
$channel->basic_consume($queue, '', false, true, false, false, function($msg) {
// push to local websocket clients
echo "server1 received: {$msg->body}\n";
});
On WebSocket Server 2: same pattern but ws.server2.queue.
Result: all websocket servers receive each message and forward to their connected users.
3. Topic Exchange
How Topic Exchange Works
A topic exchange routes messages using pattern matching on routing keys.
Topic exchanges route messages based on patterns in routing keys (dot-separated words):
Wildcards:
* matches exactly one word
# matches zero or more words
Example routing key:
embed logs.error.app1
Example bindings:
- logs.error.* ✅
- logs.*.app1 ✅
- logs.# ✅
Topic is best for:
- event buses in microservices
- routing by domain + action + sub-type
- logging/event pipelines
Laravel Example: Topic Exchange for Events
use PhpAmqpLib\Message\AMQPMessage;
public function publishEvent(string $routingKey, array $payload)
{
$connection = mqConnection();
$channel = $connection->channel();
$exchange = 'events.topic';
$channel->exchange_declare($exchange, 'topic', false, true, false);
$msg = new AMQPMessage(json_encode($payload), [
'content_type' => 'application/json',
]);
$channel->basic_publish($msg, $exchange, $routingKey);
$channel->close();
$connection->close();
}
Bind Consumers by Pattern
All logs for app1:
$queue = 'logs.app1.all.queue';
$channel->queue_declare($queue, false, true, false, false);
$channel->queue_bind($queue, 'events.topic', 'logs.#.app1');
Everything:
$queue = 'logs.all.queue';
$channel->queue_bind($queue, 'events.topic', '#');
Direct vs Fanout vs Topic: Choosing Correctly
| Requirement | Best Exchange |
|---|---|
| One specific queue processes each message | Direct |
| Broadcast to every consumer | Fanout |
| Flexible routing with patterns | Topic |
| Chat broadcast to many WebSocket nodes | Fanout |
| Event bus across services | Topic |
| Worker queues (jobs) | Direct |
Common Production Notes (Laravel):
1) Don’t auto-ack unless losing messages is acceptable
In your earlier consumer, you used no_ack=true. That makes delivery at-most-once (messages can be lost on crash). For real systems, prefer manual ack.
2) Use durable topology for reliability
Use:
- durable=true for exchanges/queues
- delivery_mode=PERSISTENT for messages
3) Prefer long-running workers (Artisan + Supervisor)
Consumers should be CLI workers (Supervisor/systemd/Docker), not HTTP requests.
Conclusion
Direct: exact routing, ideal for job workers.
Fanout: pure broadcast, perfect for chat/WebSocket clusters.
Topic: pattern routing, best for event buses and log pipelines.
Top comments (0)