RabbitMQ PRECONDITION_FAILED - inequivalent arg: redeclaring a queue with different properties

Your application logs are filling with channel exceptions and consumers or publishers keep reconnecting:

PRECONDITION_FAILED - inequivalent arg 'x-queue-type' for queue 'email.queue'
in vhost 'Email': received none but current is the value 'quorum' of type 'longstr'

This is AMQP error 406, a channel-level exception. A client declared a queue (or exchange) that already exists, but one or more of the declared properties do not match how the entity was originally created. RabbitMQ refuses the declaration and closes the channel. The connection usually survives, but most client libraries treat a channel exception as fatal and tear down the whole connection, so a single mismatched declaration often looks like a reconnect storm.

The classic triggers: you migrated queues from classic to quorum and not every client sets x-queue-type, or someone changed a TTL, max-length, or durability setting in application code without deleting the old queue first. The queue’s original definition is stored in the broker’s metadata and it does not change when you edit the code.

What this means

AMQP 0-9-1 treats queue.declare as idempotent only when it is identical. When a client declares a queue that already exists, the broker compares the incoming declaration against the stored definition. The compared properties are durable, exclusive, auto-delete, and the full arguments map (x-message-ttl, x-max-length, x-queue-type, x-dead-letter-exchange, and so on). If anything differs, the broker raises 406 PRECONDITION_FAILED on that channel and closes it. The same equivalence check applies to exchanges, where the compared properties include the exchange type and durability.

The error message names the exact argument that mismatched, the value the client sent (“received”), and the value the broker has stored (“current”). Read it literally. received none but current is the value 'quorum' means the client did not send the argument at all and the stored queue has it set.

Two details matter for diagnosis:

  1. Only client-provided arguments are compared. If a policy set the argument on the existing queue, a client that omits that argument will not trigger the exception. This is why policy-managed arguments (TTL, max-length, DLX) are the recommended pattern: they decouple the queue’s runtime behavior from what each client declares.
  2. The channel is dead, the definition is not. Retrying the same declaration on a new channel fails the same way, forever. No amount of reconnecting fixes a property mismatch. Applications in this state end up in a tight declare-fail-reconnect loop, which shows up as elevated channel and connection churn on the broker.
flowchart TD
  A[Client calls queue.declare] --> B{Queue exists?}
  B -- no --> C[Queue created with declared properties]
  B -- yes --> D{durable, exclusive, auto-delete and args match?}
  D -- yes --> E[Declare succeeds, idempotent no-op]
  D -- no --> F[406 PRECONDITION_FAILED, channel closed]
  F --> G[Client library often closes connection and reconnects]
  G --> A

Common causes

CauseWhat it looks likeFirst thing to check
Classic to quorum migration, incomplete client rolloutinequivalent arg 'x-queue-type', received none or classic, current quorumWhich clients still declare without x-queue-type
TTL or max-length changed in code, old queue still existsinequivalent arg 'x-message-ttl' or 'x-max-length', values differThe current queue arguments vs the new code
Durability flipped in codeinequivalent arg 'durable', received true but current is false (or reverse)durable column in rabbitmqctl list_queues
Mixed client versions declaring the same queueError appears intermittently, correlates with which instance connectedWhether different service versions declare different arguments
Policy-applied argument vs hardcoded argumentOne client declares an argument the queue got from an operator policyrabbitmqctl list_policies vs the client declaration
Exchange redeclared with a different typeSame 406, but naming an exchange; inequivalent arg 'type'rabbitmqctl list_exchanges type column
Quorum-incompatible features carried overx-max-priority, x-queue-mode, or x-overflow: reject-publish-dlx mismatch during quorum migrationWhether the old queue used priority, lazy mode, or reject-publish-dlx overflow

Quick checks

All read-only.

# Find the error and the exact mismatched argument in the broker log
grep -i "PRECONDITION_FAILED" /var/log/rabbitmq/rabbit@$(hostname).log | tail -20

# Inspect the stored definition of the queue named in the error
rabbitmqctl list_queues name durable auto_delete arguments | grep -F "email.queue"

# Check whether a policy is applying arguments to this queue
rabbitmqctl list_policies

# If the error names an exchange, check its stored type and durability
rabbitmqctl list_exchanges name type durable arguments

# See whether failing clients are causing channel churn on the broker
# (default guest/guest credentials only authenticate from localhost)
curl -s -u guest:guest http://localhost:15672/api/overview | jq '.churn_rates | {channel_created, channel_closed, connection_created}'

If arguments in the list_queues output is empty but the error says current is the value 'quorum', check you are looking at the right vhost: rabbitmqctl list_queues -p <vhost> ....

How to diagnose it

  1. Extract the three facts from the error message. The argument name, the received value, and the current value. In received none but current is the value 'quorum', the fix direction is either “make the client send x-queue-type: quorum” or “recreate the queue without it.” The message tells you which side disagrees.
  2. Identify the declaring client. The channel exception is logged server-side. Cross-reference the timestamp with the connection that hit it using the management API (GET /api/connections, look at user, peer_host, and client_properties). In most incidents it is one service, one version, or one freshly deployed instance.
  3. Compare against the stored definition. Run the list_queues ... arguments check above and diff it against the client’s declare call. Remember the policy exception: if a policy applies the argument, clients omitting it are fine, and the mismatch is coming from a client that sends a conflicting value.
  4. Decide which definition is authoritative. Usually the running queue with live messages is authoritative and the code is wrong. Sometimes the queue is a leftover from an old configuration and the new code is intended. This decision determines the fix path below.
  5. Check the blast radius. Look at channel and connection churn rates. A single misdeclaring client in a retry loop can generate thousands of connection attempts per hour and show up as an unrelated-looking “connection storm.”

Metrics and signals to monitor

There is no broker metric for PRECONDITION_FAILED itself. Channel and connection protocol errors are visible only in the RabbitMQ log, so log-based detection is the primary signal. The secondary signals are the churn the failure causes.

SignalWhy it mattersWarning sign
Broker log: PRECONDITION_FAILED linesThe only direct record of the exception, including the argument and valuesAny occurrence outside a planned migration
Channel churn rate (churn_rates.channel_created / channel_closed)Each failed declare kills a channel; retry loops inflate churnCreation rate sustained well above the rolling baseline
Connection churn rateMany client libraries drop the whole connection on a channel exceptionConnection creation spikes correlated with deploys
Queue churn (queue_declared)Repeated failing declares may still count as declarationsDeclared rate climbing with zero new queues created
Consumer count on the affected queueA consumer stuck in declare-fail-reconnect is not consumingConsumers below expected count while queue depth grows

Fixes

Make the client match the existing queue

The zero-data-loss fix. Update the declaring code so every property matches the stored definition: same durability, same arguments. Redeploy. This is correct when the queue holds messages you need and its current definition is the intended one.

Tradeoff: during rolling deployments you will run mixed client versions. If only some instances are updated, the old ones still throw 406 until they are replaced. That is transient and harmless as long as updated instances carry the traffic.

Recreate the queue with the new properties

The only way to change a queue’s immutable properties is to delete it and let clients redeclare it. This is destructive: deleting a queue discards every message in it. Drain it first (let consumers catch up, or shovel messages elsewhere), stop publishers if possible, then:

# DESTRUCTIVE: deletes the queue and all messages in it
rabbitmqadmin delete queue name=email.queue --vhost=Email

Then redeploy the clients with the new declaration. During the gap between deletion and the first successful declare, publishers may get unroutable messages; if publishers use mandatory=true they will see returns instead of silent loss.

Move arguments into policies, out of client code

For arguments RabbitMQ supports via policy (TTL, max-length, dead-lettering, queue type), declare them operator-side and have clients declare the bare queue. Policies can be changed without touching the queue or the clients, and because policy-applied arguments are exempt from the equivalence check, clients that omit them never trigger 406. This is the durable fix for “every service declares the same queue slightly differently.”

Tradeoff: a policy change applies to all matching queues, and applying a policy to many queues at once causes a CPU burst as the broker reconfigures them. Apply during low traffic on large deployments.

Classic to quorum migration specifics

This is the most common modern source of the error. Useful escape hatches, all version-dependent:

  • quorum_queue.property_equivalence.relaxed_checks_on_redeclaration = true in rabbitmq.conf (available since 3.11.16) makes the broker ignore x-queue-type during equivalence checks. This lets un-updated clients redeclare a queue that was converted to quorum without hitting 406. It is a migration bridge, not a permanent posture.
  • default_queue_type on the vhost (since 3.13.3) makes queues declared without an explicit type default to quorum, so old clients create new queues of the right type automatically.
  • Incompatible arguments must be cleaned up in code. Quorum queues do not support x-max-priority, exclusive queues, or x-overflow: reject-publish-dlx, and x-queue-mode: lazy is meaningless for them. A redeclaration carrying these against a quorum queue fails. Remove them from client declarations before or during migration.
  • x-consumer-timeout is not part of the equivalence check, so changing it does not cause this error. Do not chase it.
  • One behavioral surprise after migration: redeclaring a quorum queue does not renew its x-expires lease the way classic queues did. A quorum queue with queue TTL can be deleted even if clients periodically redeclare it.

Do not rapidly flip a queue’s type back and forth; there is a known race where rapid redeclaration with changing x-queue-type causes internal errors for live publishers. Migrate deliberately, once.

Prevention

  • Declare queues identically everywhere, or nowhere. Either centralize queue definitions in one component (provisioning tooling, Terraform, a topology operator), or standardize the declare call in a shared library. The failure mode is always two sources of truth.
  • Prefer policies over client arguments for anything you might want to change later. Policies are the only way to change TTL, max-length, or DLX behavior without recreating the queue.
  • Gate migrations on the declare path. When switching queue types, inventory every client that declares the affected queues before converting anything. The 406 will find the ones you missed, in production, at deploy time.
  • Alert on the log pattern. PRECONDITION_FAILED in the broker log should page during a migration window and ticket at any other time. It never self-resolves.
  • Watch channel churn after deploys. A post-deploy spike in channel churn with no obvious cause is often a declaration mismatch in the new build.

How Netdata helps

  • Netdata collects RabbitMQ object and churn rates (connections, channels, queues created and declared) from the management API, so a declare-fail-reconnect loop shows up as a channel churn anomaly you can correlate with deploy timing.
  • Per-queue metrics (messages ready, unacknowledged, consumers) show the downstream effect: a queue whose consumers are stuck in the 406 retry loop starts accumulating messages while its consumer count reads zero or flaps.
  • Connection count and churn trends let you distinguish a single misdeclaring client (churn spike, stable total) from a genuine connection storm.
  • Because Netdata keeps per-second history, you can line up the exact minute a new client version rolled out with the first churn spike and confirm the causal deploy before rolling back.
  • The 406 line itself still requires log collection; pairing log alerts on PRECONDITION_FAILED with Netdata’s churn and queue-depth charts gives you both the cause and the effect on one timeline.