What Is a Topic Exchange?
A topic exchange routes messages by matching a dot-separated routing key against binding patterns that may contain two wildcard characters: '*' (star), which matches exactly one word, and '#' (hash), which matches zero or more words. A routing key such as 'order.us-east.shipped' can be matched by binding patterns like 'order.*.shipped', 'order.#', or 'order.us-east.*', giving you hierarchical, pattern-based routing that sits between the rigid exact-match of a direct exchange and the unconditional broadcast of a fanout exchange. This makes topic exchanges the most flexible and commonly used exchange type for real-world event-driven systems where routing needs vary by consumer.
Cricket analogy: It's like a cricket commentary feed structured as 'match.innings.event' where a subscription to 'match.*.wicket' catches wickets from any innings, while 'match.2.#' catches every event from the second innings alone.
Wildcard Semantics: * and #
The star wildcard substitutes for exactly one word between dots — 'order.*.shipped' matches 'order.us-east.shipped' but not 'order.us-east.warehouse.shipped' because the star cannot span two words, and it also won't match 'order.shipped' because it requires exactly one word to be present in that position. The hash wildcard is more permissive, matching zero or more words including none at all — 'order.#' matches 'order', 'order.shipped', and 'order.us-east.warehouse.shipped' equally, which is why '#' alone as a binding key behaves identically to a fanout exchange, matching every possible routing key on that exchange.
Cricket analogy: The star is like a scorecard filter for 'session.*.wicket' matching exactly one named session (morning, afternoon) before wicket, while the hash in 'session.#' matches any number of qualifiers, including a bare 'session' entry with no detail at all.
Declaring Bindings with Wildcards
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='orders_topic', exchange_type='topic', durable=True)
result = channel.queue_declare(queue='shipping_alerts', durable=True)
# Catch every 'shipped' event regardless of region
channel.queue_bind(exchange='orders_topic', queue='shipping_alerts', routing_key='order.*.shipped')
# Catch everything happening to us-east orders, any depth
channel.queue_bind(exchange='orders_topic', queue='shipping_alerts', routing_key='order.us-east.#')
channel.basic_publish(
exchange='orders_topic',
routing_key='order.us-east.shipped',
body='Order 88213 shipped from us-east warehouse',
)
connection.close()Performance and Design Considerations
Topic exchange matching is more computationally expensive than direct exchange matching because RabbitMQ must evaluate each binding pattern's wildcard segments against the routing key rather than doing a simple hash-table lookup, though in practice this overhead is negligible for the vast majority of workloads and RabbitMQ optimizes pattern matching internally with a trie-like structure. The bigger design consideration is routing key schema discipline: because topic routing depends entirely on a consistent dot-separated naming convention, teams should document and enforce a routing key schema (e.g., '{entity}.{region}.{event}') early, since retrofitting a new segment into existing keys later breaks every existing binding pattern that assumed a fixed word count.
Cricket analogy: It's like a scoring app that must parse ball-by-ball commentary strings instead of just checking a fixed 'out/not out' flag — more parsing work per delivery, but negligible compared to the cost of getting the innings numbering scheme wrong mid-season.
A binding key of just '#' on a topic exchange matches every routing key, including keys with zero dots, making it functionally equivalent to a fanout exchange for that binding. This is a handy debugging trick: bind a temporary queue with '#' to a topic exchange to observe every message flowing through it in production without disrupting existing consumers.
- Topic exchanges route using dot-separated routing keys matched against wildcard binding patterns.
- The '*' wildcard matches exactly one word; '#' matches zero or more words.
- A binding of '#' alone matches every routing key, behaving like a fanout exchange.
- Topic exchanges sit between direct (exact match) and fanout (unconditional) in flexibility.
- Pattern matching has more overhead than direct exchange lookups, though RabbitMQ optimizes it internally.
- A consistent, documented routing key schema is essential since retrofitting new segments breaks existing bindings.
- Topic exchanges are the most commonly used exchange type in real-world event-driven RabbitMQ systems.
Practice what you learned
1. In a topic exchange binding pattern, what does the '*' wildcard match?
2. Does the routing key 'order.us-east.warehouse.shipped' match the binding pattern 'order.*.shipped'?
3. What does a binding key of just '#' on a topic exchange match?
4. Why is a consistent routing key schema important when using topic exchanges?
5. Compared to a direct exchange, how does a topic exchange's routing typically perform?
Was this page helpful?
You May Also Like
Direct Exchange Explained
Learn how RabbitMQ's direct exchange routes messages to queues using exact routing key matches, and when to reach for this exchange type.
Fanout Exchange Explained
Understand how RabbitMQ's fanout exchange broadcasts every message to all bound queues, ignoring routing keys entirely.
Bindings and Routing Keys
Understand how bindings connect exchanges to queues, and how routing keys drive delivery decisions across every RabbitMQ exchange type.
Headers Exchange Explained
Discover how RabbitMQ's headers exchange routes messages by matching message header attributes instead of routing keys, using x-match rules.