RabbitMQ Error Guide: 'NOT_FOUND - no exchange' basic.publish Failures
Fix RabbitMQ publish failures: 404 NOT_FOUND on basic.publish, returned messages, channel closed on publish, and missing publisher confirms explained.
- #rabbitmq
- #troubleshooting
- #errors
- #publishing
Exact Error Message
Publishing to a nonexistent exchange closes the channel with a 404:
Channel error on connection <0.812.0> (10.0.5.31:51544 -> 10.0.4.21:5672, vhost: '/',
user: 'app'), channel 1:
operation basic.publish caused a channel exception not_found:
no exchange 'orders.events' in vhost '/'
Client side (Java) this surfaces as:
com.rabbitmq.client.ShutdownSignalException: channel error; protocol method:
#method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange
'orders.events' in vhost '/', class-id=60, method-id=40)
A returned (unroutable, mandatory) message instead arrives via basic.return:
basic.return: reply-code=312 reply-text=NO_ROUTE exchange=orders.events
routing_key=order.created
What the Error Means
basic.publish can fail in several distinct ways, and they are easy to confuse:
- 404 NOT_FOUND: you published to an exchange that does not exist. AMQP publishing is asynchronous, so the broker reports this by closing the channel with a 404 rather than returning an error to the publish call itself.
- Returned message (312 NO_ROUTE): the exchange exists, but with the
mandatoryflag set and no queue bound for the routing key, the broker hands the message back viabasic.return. - Channel closed mid-publish: an unrelated channel exception (e.g., a precondition failure on the same channel) closes the channel, and subsequent publishes on it silently fail.
- Confirms never arriving: in publisher-confirm mode,
basic.ack/basic.nackfrom the broker do not arrive because the channel closed or confirms were never enabled correctly.
The key insight: a successful basic.publish call returning locally does not mean the broker accepted the message. Without mandatory + basic.return handling or publisher confirms, lost messages are invisible.
Common Causes
- Exchange never declared, or declared on a different vhost. The publisher assumes an exchange that a deploy never created, or connects to vhost
/while the exchange lives onapp. - Typo or environment drift in the exchange name.
orders.eventvsorders.events, or a name that differs between staging and prod. mandatoryflag with no matching binding. Correct exchange, but no queue is bound for the routing key, so the message is returned.- Publishing on a channel already closed by a prior exception. A failed
queue.declarewith mismatched arguments closes the channel; later publishes on the same channel go nowhere. - Auto-delete exchange removed after its last binding dropped. An
auto-deleteexchange vanished when its bindings were removed. - Confirms enabled per-channel but the channel was recreated. A reconnect created a fresh channel without re-calling
confirm.select, so acks never come.
How to Reproduce the Error
Publish to an exchange that was never declared:
channel.basic_publish(exchange='orders.events', routing_key='order.created', body=...)
# broker closes channel 1 with:
# 404 NOT_FOUND - no exchange 'orders.events' in vhost '/'
For a returned message, declare an exchange but bind no queue, then publish with mandatory=true:
exchange.declare(name='orders.events', type='topic')
basic.publish(exchange='orders.events', routing_key='order.created',
mandatory=true, body=...)
# -> basic.return reply-code=312 NO_ROUTE
Diagnostic Commands
# Does the exchange exist, and on which vhost?
rabbitmqctl list_exchanges name type durable --vhost / | grep -i orders
# List exchanges across all vhosts to catch vhost drift
rabbitmqctl list_exchanges --vhost app name type
# What is bound to the exchange? (empty = mandatory publishes will be returned)
rabbitmqctl list_bindings source_name routing_key destination_name | grep -i orders.events
# Channel error history and closed-channel counts
rabbitmqctl list_channels pid number state messages_unconfirmed | head -20
# Watch the broker log for basic.publish channel exceptions
journalctl -u rabbitmq-server --since "10 min ago" | grep -i 'basic.publish\|not_found\|no_route'
# Confirm the connecting user can access the target vhost
rabbitmqctl list_permissions --vhost /
messages_unconfirmed climbing without bound on a channel is the signature of confirms that are enabled but never acked. An empty binding list for the exchange is the signature of returned (NO_ROUTE) messages.
Step-by-Step Resolution
-
Read the reply code. 404 NOT_FOUND means the exchange is missing; 312 NO_ROUTE means it exists but nothing is bound. They have different fixes.
-
For 404, verify the exchange and vhost. Run
rabbitmqctl list_exchanges --vhost <v>. Confirm the name matches exactly and that the publisher connects to the same vhost. Declare the exchange as part of deployment (idempotentexchange.declare) so it always exists before publishers start. -
For 312 NO_ROUTE, check bindings. Run
rabbitmqctl list_bindings. Add the missing queue binding for the routing key, or remove themandatoryflag if returns are not desired. Always register abasic.returnhandler so returns are not silently dropped. -
For channel-closed-on-publish, isolate the original exception. A publish failure is often collateral damage from an earlier channel exception (e.g.,
406 PRECONDITION_FAILEDon a redeclare). Find the first channel exception in the log and fix that; recreate the channel afterward. -
For missing confirms, re-enable per channel.
confirm.selectis per-channel and must be called again after any reconnect. Verifymessages_unconfirmedis being drained by acks. -
Add publisher confirms for durability. Enable confirms and treat unacked publishes as failures to retry. This is the only reliable way to know the broker accepted a message.
-
Verify. Re-publish and confirm the channel stays open and
basic.ackarrives (orbasic.returnfires for unroutable messages you expect).
Prevention and Best Practices
- Declare exchanges and bindings idempotently at startup so topology never depends on manual setup.
- Always enable publisher confirms for messages that matter; a returned publish call is not proof of delivery.
- Set
mandatory=trueand handlebasic.returnso unroutable messages are logged or retried, not lost. - Pin exchange and vhost names in config and validate them across environments to catch drift.
- Use a dedicated channel per concern so one channel exception does not silently break unrelated publishes.
- Alert on rising
messages_unconfirmedand on anybasic.returnrate above zero.
Related Errors
- no route / unroutable message: the dedicated NO_ROUTE case when
mandatoryis set and nothing is bound. - publisher nack received: the broker accepted then rejected via
basic.nackunder confirms — a different failure than 404. - resource alarm: under an alarm, publishes block rather than fail with a channel error.
- access_refused (403): the user lacks write permission on the exchange or vhost, a different reply code than 404.
Frequently Asked Questions
Why does my publish call succeed but the message disappears?
AMQP publishing is fire-and-forget by default. Without publisher confirms and mandatory + basic.return, a 404 or NO_ROUTE only closes the channel or returns the message asynchronously — the publish call itself does not raise.
Does declaring the exchange in the publisher fix the 404?
Yes, if done idempotently before publishing. Call exchange.declare with passive=false at startup so the exchange always exists.
What is the difference between a returned message and a nack?
A return (basic.return, 312) means the broker could not route the message to any queue. A nack (basic.nack) means the broker accepted routing but could not persist or enqueue it.
Why do confirms stop arriving after a reconnect?
confirm.select is per-channel. A reconnect creates a new channel that is not in confirm mode until you call confirm.select again.
Can a single bad publish break other publishes? Yes — if they share a channel. The 404 closes the entire channel, so every subsequent publish on it fails until the channel is recreated.
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.