RabbitMQ Error Guide: 'PRECONDITION_FAILED - inequivalent arg for exchange' Declaration Conflict
Fix RabbitMQ PRECONDITION_FAILED inequivalent arg errors on exchange declare or delete: mismatched type, durable, auto-delete, and internal flags.
- #rabbitmq
- #troubleshooting
- #errors
- #exchanges
Exact Error Message
When a client redeclares an existing exchange with even one attribute different from how it was originally created, RabbitMQ refuses the operation and closes the channel:
Channel error on connection <0.31415.0> (10.0.4.12:54322 -> 10.0.4.7:5672, vhost: '/', user: 'app'):
operation exchange.declare caused a channel exception precondition_failed:
inequivalent arg 'type' for exchange 'events' in vhost '/':
received 'topic' but current is 'direct'
Closely related variants you may see on a delete or a durability mismatch:
operation exchange.declare caused a channel exception precondition_failed:
inequivalent arg 'durable' for exchange 'events' in vhost '/':
received 'false' but current is 'true'
operation exchange.delete caused a channel exception precondition_failed:
cannot delete exchange 'events' in vhost '/': it is in use as a binding source
What the Error Means
exchange.declare is an idempotent assertion, not a “create if missing” call. When a client declares an exchange that already exists, the broker compares every property in the request against the live definition: type, durable, auto_delete, internal, and any arguments (such as alternate-exchange). If any one of them differs, RabbitMQ raises a PRECONDITION_FAILED (AMQP reply code 406) channel exception rather than silently mutating the existing exchange.
This is a safety mechanism. An exchange’s type and durability cannot be changed in place — altering them would invalidate existing bindings and persistence guarantees. So instead of mutating, the broker rejects the mismatched declare and forces you to reconcile the definitions. The channel that triggered the exception is closed; the connection survives, but any in-flight work on that channel is lost.
The “cannot delete exchange” form is a different but adjacent precondition: an exchange.delete with if-unused=true fails because the exchange still serves as a binding source for one or more queues or other exchanges.
Common Causes
- Two services declare the same exchange with different types. One team ships
direct, another shipstopicfor the same logical name. - A durability or auto-delete drift. A producer declares
durable: truein production but a test harness or a second service declares the same name asdurable: false. - An infrastructure-as-code definition diverged from application code. The exchange was created by a
definitions.jsonimport with one set of arguments, and the app redeclares it differently on startup. - An
internalflag mismatch when an exchange is meant to receive only exchange-to-exchange bindings. - A changed
alternate-exchangeargument between deploys. - Deleting an exchange that is still bound (the
cannot deletevariant) when other queues or exchanges still source messages from it.
How to Reproduce the Error
Declare an exchange one way, then redeclare it with a different type from a second connection:
# First declaration: a durable direct exchange
rabbitmqadmin declare exchange name=events type=direct durable=true
# Second declaration with a different type -> PRECONDITION_FAILED
rabbitmqadmin declare exchange name=events type=topic durable=true
The second command returns a 406 and the channel is closed. The same happens if you keep type=direct but flip durable=false. To reproduce the delete variant, bind a queue to the exchange and then attempt rabbitmqadmin delete exchange name=events while a binding still sources from it.
Diagnostic Commands
Inspect the current, authoritative definition of the exchange so you know exactly what the broker expects:
# List every exchange with its type, durability, auto-delete, internal, and arguments
rabbitmqctl list_exchanges name type durable auto_delete internal arguments
# Same view scoped to a single vhost
rabbitmqctl list_exchanges --vhost / name type durable arguments
name type durable auto_delete internal arguments
events direct true false false []
Confirm what bindings depend on the exchange before any delete:
# Which queues/exchanges are bound from this exchange (it is the source)
rabbitmqctl list_bindings source_name destination_name destination_kind routing_key | grep events
Check the broker log for the precise inequivalent argument and which connection sent it:
sudo journalctl -u rabbitmq-server --since '-15min' | grep -i 'precondition_failed\|inequivalent'
inequivalent arg 'type' for exchange 'events' in vhost '/': received 'topic' but current is 'direct'
Step-by-Step Resolution
Step 1: Read the error to find the offending argument
The message names the exact field (type, durable, etc.), the received value (what your client sent), and the current value (what the broker holds). Treat current as the source of truth unless you have decided to change the definition deliberately.
Step 2: Decide which definition is correct
If most of the system expects direct and one new service sends topic, fix the new service. If you genuinely need the new type, you must plan a migration — you cannot redeclare your way into a new type.
Step 3: Align the client code
Update the declaring client so every property matches the live exchange exactly. Make sure shared exchange definitions live in one place (a config module or a definitions.json) so producers and consumers cannot drift.
# Re-verify the live definition you must match
rabbitmqctl list_exchanges name type durable auto_delete internal arguments | grep events
Step 4: To intentionally change type or durability, recreate the exchange
Because type and durability are immutable, migrate during a maintenance window: record the bindings, delete the exchange (and recreate consumers’ bindings against the new one), then redeclare with the desired properties.
# Capture existing bindings first
rabbitmqctl list_bindings source_name destination_name routing_key | grep events
Step 5: For the “cannot delete” variant, drop dependents first
If exchange.delete fails because the exchange is in use, remove the bindings (or delete it without if-unused) once you have confirmed nothing still needs to route through it.
Step 6: Confirm the channel no longer faults
Restart or reconnect the corrected client and watch the log for a clean declare with no further precondition_failed entries.
Prevention and Best Practices
- Define exchanges in exactly one place. Centralize names, types, and arguments so producers and consumers cannot diverge.
- Prefer importing a
definitions.jsonat provisioning time and have applications declare passively (or declare with identical args everywhere). - Treat
exchange.declareas an assertion. Never expect it to “update” an exchange — it only confirms a match. - Pin durability and type in shared constants rather than per-service literals.
- Use a startup health check that lists the live exchange and compares it to expected values, failing fast with a clear message instead of a cryptic channel close.
- When you must change a type, version the name (for example
events.v2) so the migration is explicit and reversible.
Related Errors
- PRECONDITION_FAILED - inequivalent arg for queue — the queue-side equivalent, where
x-message-ttl,durable, orx-queue-typemismatches close the channel. - NOT_FOUND - no exchange — when a passive declare or publish targets an exchange that does not exist at all.
unknown exchange type— a plugin-provided type (such asx-delayed-message) is requested but the plugin is disabled.
Frequently Asked Questions
Can I change an exchange’s type without deleting it? No. Type is immutable. You must delete and recreate the exchange (re-establishing bindings), which is why a versioned name is the safest migration path.
Does this error drop my connection? No — it closes only the channel that issued the bad declare. Well-behaved clients open a fresh channel, but the failed operation is lost and must be retried against a corrected definition.
Why does the broker not just update the exchange to match my request? Because doing so would silently break other clients relying on the original type, durability, or arguments. Rejecting the mismatch keeps the topology predictable.
The error says ‘cannot delete exchange … in use’. How do I force it? Delete without the if-unused precondition, or remove the dependent bindings first. Confirm with list_bindings that nothing critical still routes through it before you do.
How do I find which service is sending the wrong declaration? The broker log line that precedes the exception includes the client connection’s IP and port. Map that back to the offending service and align its exchange definition.
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.