Automatic Retries
Automatic retries let the broker redeliver rejected messages after a growing backoff delay, replacing the manual pattern of chaining queues with TTLs and dead letter exchanges.
How It Works
- Declare a queue with the
x-delayed-retry-minargument to enable the feature - When a consumer rejects a message with
requeue=true(viabasic.rejectorbasic.nack), the broker delays it in an internal retry queue (amq.retry-<queue>) instead of requeuing it immediately - When the backoff delay expires, the message is redelivered to the queue
- When the delivery count exceeds
x-delivery-limit, the message is dead-lettered (or dropped without a dead letter exchange)
Queue Arguments
| Argument | Type | Default | Description |
|---|---|---|---|
x-delayed-retry-min |
integer (≥ 1, ms) | - | Initial delay before the first retry. Setting it enables the feature. |
x-delayed-retry-multiplier |
integer (≥ 1) | unset (linear) | Backoff shape: omit for linear (delay × attempt), 1 for constant, ≥ 2 for exponential (delay × multiplier^(attempt-1)). |
x-delayed-retry-max |
integer (≥ 1, ms) | unset (no cap) | Cap on a single retry delay. Only clamps the delay, never ends retries. |
x-delivery-limit |
integer (≥ 0) | 20 when retry is enabled | Maximum number of redeliveries: a message is delivered at most x-delivery-limit + 1 times before dead-lettering. An explicit value, including 0, is respected. |
Delays are additionally capped at the UInt32 millisecond range (~49.7 days) regardless of x-delayed-retry-max.
Example
# Up to 6 deliveries, backoff starting at 500 ms, doubling each time, capped at 30 s
x-delivery-limit: 5
x-delayed-retry-min: 500
x-delayed-retry-multiplier: 2
x-delayed-retry-max: 30000
x-dead-letter-exchange: ""
x-dead-letter-routing-key: failed-messages
Retry delays are 500 ms, 1 s, 2 s, 4 s, 8 s. When the 6th delivery is also rejected, the message is routed to the failed-messages queue with x-death reason delivery_limit.
What Triggers a Retry
| Consumer action | Behavior |
|---|---|
basic.reject(requeue=true) / basic.nack(requeue=true) |
Delayed in the retry queue, redelivered after the backoff. |
basic.reject(requeue=false) / basic.nack(requeue=false) |
Straight to the dead letter exchange, or dropped. |
Channel/connection close, basic.recover |
Instant requeue with no backoff, but the redelivery still counts towards x-delivery-limit. |
The x-delivery-count header on a delivery tells the consumer how many deliveries preceded it; it is absent on the first delivery. x-delivery-limit is always compared against this one counter, which increments on every redelivery regardless of cause, so delayed retries and instant broker-initiated requeues consume the same budget. The counter is maintained by the broker alone: an x-delivery-count header supplied on a publish to a retry-enabled queue is removed. A message dead-lettered into a retry-enabled queue therefore always starts with a fresh retry budget.
Retry Queue
Each retry-enabled queue gets an internal companion queue named amq.retry-<queue>, holding delayed messages ordered by redelivery time. It is created with the queue, deleted with it, and recreated automatically if it disappears. Like all internal queues it cannot be operated on by AMQP clients, but it is visible in the management UI and HTTP API, where the number of delayed messages can be monitored.
Notes and Limitations
- Retry delays are minimums, not exact schedules: under heavy load redelivery can lag behind the configured delay
- The message’s original timestamp is preserved through retries, so
x-message-ttlapplies to the message’s total age and can expire a message mid-retry - Retry cannot be combined with
x-message-deduplication: the declaration is refused, since a retried message would always be dropped as a duplicate - Redeliveries caused by consumer disconnects consume the delivery budget, so pick
x-delivery-limitwith restart frequency in mind - If the queue is full with
overflow=reject-publishwhen a retry is due, the message stays in the retry queue and is delayed again for one more backoff period - The retry arguments can only be set at queue declaration, not via policies, and are refused on streams
- If the retry queue cannot store a message, for example on a disk error, the message is requeued instantly without backoff and the retry queue is recreated on the next reject
- The queue name must leave room for the
amq.retry-prefix within the 255 byte queue name limit
Ready to take the next steps?
Managed LavinMQ instance via CloudAMQP
LavinMQ has been built with performance and ease of use in mind - we've benchmarked a throughput of about 1,000,000 messages/sec. You can try LavinMQ without any installation hassle by creating a free instance on CloudAMQP. Signing up is a breeze.
Get started with CloudAMQP ->Help and feedback
We welcome your feedback and are eager to address any questions you may have about this piece or using LavinMQ. Join our Slack channel to connect with us directly. You can also find LavinMQ on GitHub.