Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for RabbitMQ By James Joyner IV · · 8 min read

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.return with reply code 312 / NO_ROUTE.
  • Messages published without mandatory simply 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=true and handle basic.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: direct for exact keys, topic with deliberate */# patterns, fanout for broadcast — and review binding keys when adding new routing-key shapes.
  • Add an alternate-exchange (alternate-exchange argument) 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:

  1. No queue is bound to the exchange at all.
  2. The routing key doesn’t match a direct exchange’s binding key.
  3. A topic wildcard pattern doesn’t span the routing key’s segments.
  4. Publishing to the wrong exchange (or confusing the default exchange).
  5. An exchange-type mismatch vs. how the publisher uses routing keys.
  6. 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.

Free download · 368-page PDF

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.