RabbitMQ Error Guide: 'NO_ROUTE' Mandatory Message Returned Unroutable
Fix RabbitMQ NO_ROUTE returned messages: diagnose missing bindings, wrong routing keys, default vs named exchange confusion, and exchange type mismatches.
- #rabbitmq
- #troubleshooting
- #errors
- #routing
Overview
NO_ROUTE is what a publisher sees when it sends a message with the mandatory flag set and the exchange cannot route it to any queue. Instead of silently dropping the message (the default for unroutable messages), the broker returns it to the publisher via a basic.return carrying reply code 312 and the text NO_ROUTE. This is not a broker fault — it is the broker telling you your topology and your routing key don’t line up: no binding matched, so the message had nowhere to go.
You will see it in the publisher as a returned message:
basic.return: reply-code=312 reply-text=NO_ROUTE
exchange='orders' routing-key='order.created.eu'
Or in a client library callback:
Message returned: 312 NO_ROUTE — exchange 'orders', routing key 'order.created.eu'
Without mandatory, the same message would vanish silently — which is why unexpected message loss is often really a NO_ROUTE that was never surfaced. The error always names the exchange and routing key, which is exactly what you compare against the exchange’s bindings.
Symptoms
- Publisher receives a
basic.returnwith reply code 312 /NO_ROUTE. - Messages published without
mandatorysimply disappear (no consumer ever sees them). - The exchange exists and accepts publishes, but no queue receives them.
rabbitmqctl list_bindings | grep orders
orders exchange orders.eu queue order.created []
The only binding routes order.created, but the publisher uses routing key order.created.eu — no match, so NO_ROUTE.
Common Root Causes
1. No queue is bound to the exchange
The exchange exists but has zero bindings, so nothing can ever match.
rabbitmqctl list_bindings | awk '$1=="events"'
(no rows)
The events exchange has no bindings — every mandatory publish to it returns NO_ROUTE until a queue is bound.
2. Routing key doesn’t match the binding key
For direct/topic exchanges, the message routing key must match the binding key (exactly for direct, by pattern for topic).
rabbitmqctl list_bindings | grep payments
payments exchange payments.q queue payment.processed []
A publisher using payment.complete (not payment.processed) on a direct payments exchange gets NO_ROUTE — direct routing requires an exact match.
3. Topic wildcard pattern doesn’t cover the key
A topic binding’s */# pattern doesn’t span the segments of the routing key.
rabbitmqctl list_bindings | grep logs
logs exchange logs.error queue *.error []
A message keyed app.db.error (three segments) won’t match *.error (which matches exactly two segments); #.error or *.*.error would. Hence NO_ROUTE.
4. Publishing to the wrong exchange (or the default exchange)
Publishing to the default exchange ("") routes by queue name; publishing to a named exchange that isn’t bound the way you expect doesn’t.
rabbitmqctl list_exchanges name type | grep -E 'orders|^\s'
direct
orders topic
If the app means to use the default exchange (route directly to a queue named in the routing key) but publishes to orders instead, and orders has no matching binding, the message returns NO_ROUTE.
5. Exchange type mismatch vs. how it’s used
A fanout exchange ignores routing keys (broadcasts to all bound queues); a direct needs exact keys. Using the wrong mental model leaves messages unrouted.
rabbitmqctl list_exchanges name type | grep broadcast
broadcast direct
The team treats broadcast as fanout and publishes with an empty routing key, but it’s actually direct with keyed bindings — empty key matches nothing, so NO_ROUTE.
6. The binding exists in a different vhost
The queue is bound to the exchange, but in another vhost than the publisher connected to.
for v in / orders; do echo "== $v =="; rabbitmqctl list_bindings -p "$v" 2>/dev/null | grep shipments; done
== / ==
== orders ==
orders exchange shipments.q queue shipment.new []
The binding lives in vhost orders, but the publisher is on / where the exchange has no such binding — NO_ROUTE in /.
Diagnostic Workflow
Step 1: Read the exchange and routing key from the returned message
The basic.return names both: exchange='X' routing-key='Y'. These two values, compared against the exchange’s bindings, almost always reveal the mismatch directly.
Step 2: List the exchange’s bindings and binding keys
rabbitmqctl list_bindings | awk -v ex=<EXCHANGE> '$1==ex'
You get every (routing/binding key -> queue) for that exchange. If the list is empty, nothing is bound. If keys exist, compare them to the message’s routing key.
Step 3: Confirm the exchange type and matching rules
rabbitmqctl list_exchanges name type | grep <EXCHANGE>
direct needs exact key matches; topic matches by */# pattern; fanout ignores the key entirely. Verify the publisher’s routing-key strategy fits the type.
Step 4: Check you’re in the right vhost
rabbitmqctl list_connections name vhost | grep <PUBLISHER_IP>
rabbitmqctl list_bindings -p <VHOST> | grep <EXCHANGE>
A binding that exists in another vhost won’t help a publisher connected to /.
Step 5: Add the missing binding or fix the key, then retest
# bind a queue with the key the publisher actually uses:
rabbitmqadmin -V <VHOST> declare binding source=<EXCHANGE> \
destination=<QUEUE> destination_type=queue routing_key='<KEY>'
Or correct the publisher’s routing key / target exchange so it matches an existing binding, then publish a test message and confirm no return.
Example Root Cause Analysis
A new order.created.eu event type is added to an order service. Producers publish to the topic exchange orders with mandatory=true, and immediately every EU event comes back as 312 NO_ROUTE. The original order.created (no region suffix) routes fine.
Listing the bindings:
rabbitmqctl list_bindings | awk '$1=="orders"'
orders exchange order-eu.q queue order.created []
orders exchange order-us.q queue order.created []
The bindings use the literal key order.created, not a topic pattern — so they only match the exact key order.created, not the new order.created.eu. Because the team intended topic routing, the binding keys should be patterns. Fix: rebind the EU queue with a pattern that captures the region suffix:
rabbitmqadmin declare binding source=orders destination=order-eu.q \
destination_type=queue routing_key='order.created.*'
Now order.created.eu matches order.created.* and routes to order-eu.q. The US queue is similarly rebound or kept on the literal key as appropriate. The returns stop because there is finally a binding whose key matches the publisher’s routing key. The mandatory flag is what surfaced the gap instead of silently dropping every EU order.
Prevention Best Practices
- Publish critical messages with
mandatory=trueand handlebasic.return— silent NO_ROUTE is the most common cause of “lost” messages. - Define exchanges, queues, and bindings from one source (definitions JSON or an init job) so a new routing key can’t ship without a matching binding.
- Match exchange type to intent:
directfor exact keys,topicwith deliberate*/#patterns,fanoutfor broadcast — and review binding keys when adding new routing-key shapes. - Add an alternate-exchange (
alternate-exchangeargument) to capture unroutable messages into a catch-all queue, so nothing is lost while you fix bindings. - Test new routing keys against existing bindings in CI before deploy, and include the vhost explicitly in publisher config.
- When NO_ROUTE returns spike, the free incident assistant can diff the returned routing key against the exchange’s bindings to pinpoint the gap. More patterns in the RabbitMQ guides.
Quick Command Reference
# Bindings for the exchange (key -> queue)
rabbitmqctl list_bindings | awk -v ex=<EXCHANGE> '$1==ex'
# Exchange type and routing rules
rabbitmqctl list_exchanges name type | grep <EXCHANGE>
# Publisher's vhost vs. where bindings live
rabbitmqctl list_connections name vhost | grep <PUBLISHER_IP>
rabbitmqctl list_bindings -p <VHOST> | grep <EXCHANGE>
# Add the missing binding
rabbitmqadmin -V <VHOST> declare binding source=<EXCHANGE> \
destination=<QUEUE> destination_type=queue routing_key='<KEY>'
# Catch-all for unroutable messages
rabbitmqadmin declare exchange name=<EXCHANGE> type=topic durable=true \
arguments='{"alternate-exchange":"unrouted"}'
Conclusion
NO_ROUTE (reply code 312) is the broker returning a mandatory message that no binding could route. It is a topology/routing-key mismatch, not a broker fault. The usual root causes:
- No queue is bound to the exchange at all.
- The routing key doesn’t match a direct exchange’s binding key.
- A topic wildcard pattern doesn’t span the routing key’s segments.
- Publishing to the wrong exchange (or confusing the default exchange).
- An exchange-type mismatch vs. how the publisher uses routing keys.
- The binding exists in a different vhost than the publisher.
The returned message names the exchange and routing key — compare those to list_bindings and the exchange type, then add the missing binding or fix the key. Keep mandatory on so the gap surfaces instead of silently dropping messages.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.