February 20, 2026•8 min read•RabbitMQ tutorial

RabbitMQ has four built-in exchange types: direct, fanout, topic, and headers. Each uses different routing rules to decide which queues receive a message.
This guide covers what each exchange type does, how routing keys and wildcards work, and how to pick the right one, with animated diagrams and runnable examples.
| Exchange | Routing logic | Routing key | Use case |
|---|---|---|---|
| Direct | Exact key match | Exact match | Task queues, command routing |
| Fanout | Broadcast to all | Ignored | Notifications, cache invalidation |
| Topic | Wildcard pattern match | Pattern match | Event systems, hierarchical routing |
| Headers | Header attribute match | Ignored | Multi-dimensional filtering |
An exchange is the routing layer in RabbitMQ. Producers never send messages directly to queues. Instead, they publish messages to an exchange, which then routes them to one or more queues based on rules called bindings and routing keys.
The flow looks like this:
Producer → Exchange → Queue → Consumer
More precisely, the binding sits between the exchange and the queue:
Producer → Exchange → Binding (routing key) → Queue → Consumer
Every exchange has a type that determines how it evaluates routing keys and delivers messages. RabbitMQ ships with four built-in exchange types: direct, fanout, topic, and headers. Exchanges, queues, and bindings are all part of the AMQP protocol that RabbitMQ implements.
An exchange receives messages from producers and decides where to route them. A queue stores messages until they are consumed.
| Exchange | Queue | |
|---|---|---|
| Receives messages from producers | ✅ | Usually no |
| Routes messages | ✅ | ❌ |
| Stores messages | ❌ | ✅ |
| Delivers to consumers | ❌ | ✅ |
| Uses routing rules | ✅ | ❌ |
Put together:
Producer → Exchange → Queue → Consumer
An exchange holds nothing. If no queue is bound to it with a matching binding, a published message is simply dropped. A queue, on the other hand, keeps messages until a consumer acknowledges them, see message acknowledgment in RabbitMQ.
A queue can receive messages directly in one case only: the default exchange, which makes sendToQueue look like publishing straight to a queue.
A routing key is a string attached to a message when it is published to an exchange. Depending on the exchange type, RabbitMQ uses the routing key to determine which queues should receive the message.
The routing key is set by the producer at publish time. Each binding also has a binding key, and the exchange type decides how the two are compared:
| Exchange | Routing key behavior |
|---|---|
| Direct | Exact match |
| Fanout | Ignored |
| Topic | Wildcard pattern matching |
| Headers | Ignored |
By convention, routing keys are dot-delimited words such as order.created.eu. This is mandatory for topic exchanges and merely a good habit everywhere else.
A direct exchange routes messages to queues whose binding key exactly matches the message's routing key. This is the simplest and most common exchange type.
In this example, the queues have the same name as the routing key, but this is not mandatory. The important part is that the binding key of the queue matches exactly the routing key of the message for it to be delivered.
If a message is published with a routing key that doesn't match any binding key, it gets dropped (or dead-lettered if the exchange has a dead-letter exchange configured, see setting up dead-letter queues).
await channel.assertExchange("orders", "direct");
await channel.assertQueue("order.created");
await channel.bindQueue("order.created", "orders", "order.created");
await channel.assertQueue("order.cancelled");
await channel.bindQueue("order.cancelled", "orders", "order.cancelled");
channel.publish("orders", "order.created", Buffer.from("new order"));In this example, only the order.created queue receives the message because the routing key matches exactly.
When multiple queues are bound to the same exchange with the same routing key, the message is delivered to all matching queues. This allows for multiple consumers processing the same type of message.
A fanout exchange broadcasts every message to all bound queues, regardless of routing keys. Routing keys are completely ignored.
await channel.assertExchange("notifications", "fanout");
await channel.assertQueue("email_notifications");
await channel.bindQueue("email_notifications", "notifications", "");
await channel.assertQueue("push_notifications");
await channel.bindQueue("push_notifications", "notifications", "");
channel.publish("notifications", "", Buffer.from("new notification"));Both email_notifications and push_notifications receive the message.
A RabbitMQ topic exchange routes messages using wildcard patterns in routing keys. Use * to match exactly one word and # to match zero or more words.
Routing keys must be dot-delimited words (e.g., order.created.eu), and the wildcards live in the binding key, not in the routing key of the published message.
* vs #| Wildcard | Meaning | Example |
|---|---|---|
* | Exactly one word | order.*.created |
# | Zero or more words | order.# |
Concrete examples, assuming three-word routing keys like order.created.eu:
order.# matches order.created.eu, order.canceled.fr, and order itself, but not user.created.eu. # also matches zero words.*.*.eu matches order.created.eu and user.created.eu, but not order.eu (two words) or order.created.eu.b2b (four words). Each * must consume exactly one word.*.created.* matches order.created.eu and user.created.ca, but not order.created (missing the third word) or user.deleted.ca.await channel.assertExchange("events", "topic");
await channel.assertQueue("all_order_events");
await channel.bindQueue("all_order_events", "events", "order.#");
await channel.assertQueue("eu_events");
await channel.bindQueue("eu_events", "events", "*.*.eu");
await channel.assertQueue("created_events");
await channel.bindQueue("created_events", "events", "*.created.*");
channel.publish("events", "order.created.eu", Buffer.from("EU order"));In this example, all three queues receive the message because:
order.# matches any key starting with order.*.*.eu matches any three-word key ending in eu*.created.* matches any three-word key with created in the middleA message that matches several bindings is delivered once per matching queue, never twice to the same queue.
logs.error.eu, logs.info.us)# makes a topic exchange behave like a fanout exchange (matches everything)A headers exchange routes messages based on message header attributes instead of routing keys. The routing key is completely ignored.
When binding a queue to a headers exchange, you specify a set of key-value pairs and a matching mode:
x-match: all: the message must contain all specified headers with matching valuesx-match: any: the message must contain at least one matching headerawait channel.assertExchange("imports", "headers");
await channel.assertQueue("csv_imports");
await channel.bindQueue("csv_imports", "imports", "", {
"x-match": "all",
format: "csv",
source: "upload",
});
await channel.assertQueue("any_upload");
await channel.bindQueue("any_upload", "imports", "", {
"x-match": "any",
source: "upload",
source: "api",
});
channel.publish("imports", "", Buffer.from("data"), {
headers: { format: "csv", source: "upload" },
});Both queues receive the message: csv_imports because both headers match, and any_upload because source: "upload" matches.
Headers exchanges are the least commonly used type due to their added complexity and slightly lower performance compared to topic exchanges.
RabbitMQ's default exchange is an unnamed direct exchange represented by an empty string (""). Every queue is automatically bound to it using its queue name as the routing key.
This is why sendToQueue works without declaring an exchange:
channel.sendToQueue("my_queue", Buffer.from("hello"));
// equivalent to:
channel.publish("", "my_queue", Buffer.from("hello"));Two things to know about it:
If you just want to drop a message on a queue by hand, see how to manually publish messages to RabbitMQ.
| If you need... | Use |
|---|---|
| Exact routing | Direct |
| Broadcast to everyone | Fanout |
| Flexible patterns | Topic |
| Header-based filtering | Headers |
A good rule of thumb: start with direct for simple routing, move to topic when you need pattern-based flexibility, use fanout for broadcast scenarios, and reserve headers for edge cases where key-based routing falls short. More guidance on topology design in our RabbitMQ best practices.
Direct, fanout, topic, and headers. Direct matches the routing key exactly, fanout ignores it and broadcasts to every bound queue, topic matches wildcard patterns, and headers routes on message header attributes.
An exchange receives messages from producers and routes them using bindings. A queue stores messages until a consumer reads them. Exchanges never store anything, queues never route anything.
A topic exchange routes messages by matching dot-delimited routing keys against wildcard binding patterns, where * matches exactly one word and # matches zero or more words.
The default exchange is an unnamed direct exchange ("") to which every queue is automatically bound by its own name. Publishing to "" with a routing key equal to a queue name delivers straight to that queue.
A routing key is a string attached to a message at publish time. Direct exchanges match it exactly, topic exchanges match it against wildcard patterns, and fanout and headers exchanges ignore it.
A direct exchange. It delivers a message only to queues whose binding key is exactly equal to the message's routing key.
Yes. A queue can have any number of bindings, on the same exchange or across different exchanges and different exchange types. Each matching binding delivers a copy, but the same queue never receives the same message twice from one publish.
Understanding how messages flow through your exchanges is critical for debugging routing issues. We wrote an entire article dedicated to this topic: How to inspect RabbitMQ exchanges in production. It covers how to use RabbitGUI to visualize your exchanges, bindings, and message flow in real-time. To watch live traffic without consuming the main queue, see how to spy on real-time queue traffic, or browse messages without consuming them.
RabbitMQ tutorialRabbitMQ Delayed MessagesLearn how to implement delayed messages in RabbitMQ using the delayed message exchange plugin and the message TTL with dead-letter queue pattern.
RabbitMQ tutorialRabbitMQ Monitoring APIComplete documentation on how to monitor RabbitMQ using its HTTP monitoring API with detailed explanations of available metrics and examples.
RabbitMQ tutorialRabbitMQ ACK vs NACK: acknowledgements, requeue & reject explainedLearn how RabbitMQ ACK, NACK and reject work. Understand manual vs auto acknowledgements, requeue=true, redelivery, prefetch and how to avoid infinite requeue loops.Debug, monitor, and manage RabbitMQ with a modern developer interface.
Available on Windows, Mac, and Linux.

Cheat sheetRabbitMQ Javascript Cheat-SheetEverything you need to know to get started with RabbitMQ in NodeJs and Docker with code examples ready to go.
ProductHow to log into your CloudAMQP RabbitMQ instanceUse RabbitGUI to connect to your CloudAMQP instance and manage your dead letter queues with ease
ProductHow security is built into RabbitGUIRabbitGUI was built with security as a top priority for its users, and here is how it was done!