skip to content

Dead-Letter & Retry

Dead-letter exchanges plus retry interceptors keep a message that always fails from being requeued forever. Interviewers ask about the infinite redelivery loop, because everyone has caused one at least once.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

What is dead-lettering in RabbitMQ/Spring AMQP, and how do you configure a dead-letter exchange on a queue?

level: juniorimportance: must knowfreq 70%

answer

  1. x-dead-letter-exchange queue arg
  2. reject/nack requeue=false, TTL, max-length
  3. DLX -> DLQ for inspection
  4. x-death header count/reason
  5. arguments set at declare time, immutable

basics

~10 s

Dead-lettering routes messages that can't be processed to a separate exchange instead of losing them. You set the queue arguments x-dead-letter-exchange (and optionally x-dead-letter-routing-key) when declaring the queue.

solid answer

~30 s

Dead-lettering is RabbitMQ's mechanism for shunting undeliverable messages to a designated 'dead-letter exchange' (DLX) so they aren't silently dropped. You attach it by declaring the main queue with the arguments x-dead-letter-exchange and optionally x-dead-letter-routing-key. A message is dead-lettered when: it's rejected/nacked with requeue=false, its per-message or per-queue TTL expires, or the queue exceeds a length limit. From the DLX it's routed to a dead-letter queue (DLQ) for inspection or reprocessing. In Spring AMQP you set these via QueueBuilder.withArgument(...) or Queue arguments in the bean definition. RabbitMQ adds an x-death header recording why and how often a message was dead-lettered.

code

java · 28 lines
java
@Configuration
class DlxConfig {

    @Bean
    Queue ordersQueue() {
        return QueueBuilder.durable("orders")
            .withArgument("x-dead-letter-exchange", "orders.dlx")
            .withArgument("x-dead-letter-routing-key", "orders.dead")
            .build();
    }

    @Bean
    DirectExchange ordersDlx() {
        return new DirectExchange("orders.dlx");
    }

    @Bean
    Queue ordersDlq() {
        return QueueBuilder.durable("orders.dlq").build();
    }

    @Bean
    Binding dlqBinding() {
        return BindingBuilder.bind(ordersDlq())
            .to(ordersDlx())
            .with("orders.dead");
    }
}

go deeper

for a junior

Know that dead-lettering saves failed messages to a separate queue and it's configured via the x-dead-letter-exchange queue argument.

for a middle

Know the three trigger conditions (reject-no-requeue, TTL, max-length) and that arguments are immutable at declare time.

for a senior

Explain the x-death header, DLX vs DLQ binding, and how the listener's exception maps to the reject that triggers dead-lettering.

for a principal

Design the topology (naming, bindings, alerting) and use x-death for delayed-retry loops and poison-message policy.

**Dead-lettering** is RabbitMQ's built-in safety net for messages that cannot be processed. Instead of discarding them, the broker re-routes them to a **dead-letter exchange (DLX)** — an ordinary exchange you nominate — which typically fans them into a **dead-letter queue (DLQ)** where you can inspect, alert on, or replay them. **How you configure it:** A DLX is not a special server-wide setting; it's a *queue argument* set when the queue is **declared** (arguments are immutable afterward — you must delete and recreate the queue to change them): - `x-dead-letter-exchange` — the name of the exchange dead-lettered messages are published to. - `x-dead-letter-routing-key` — optional; overrides the routing key used when republishing to the DLX. If omitted, the message's *original* routing key is reused. **When does a message get dead-lettered?** Three triggers: 1. The consumer **rejects** it with `basic.reject` or `basic.nack` and `requeue=false`. 2. The message's **TTL expires** (per-message `expiration` or the queue's `x-message-ttl`). 3. The queue hits a **length limit** (`x-max-length` / `x-max-length-bytes`) and the message is dropped from the head. **The x-death header:** When RabbitMQ dead-letters a message it stamps (or updates) an `x-death` header — an array of entries per queue/reason recording the `count`, `reason` (rejected / expired / maxlen), `queue`, `time`, original `exchange`, and `routing-keys`. This is how you can build retry-count logic or delayed-retry loops. **Spring AMQP specifics:** You declare the arguments on the `Queue` bean. Modern idiom uses `QueueBuilder`: ```java QueueBuilder.durable("orders") .withArgument("x-dead-letter-exchange", "orders.dlx") .withArgument("x-dead-letter-routing-key", "orders.dead") .build(); ``` Spring's `RabbitAdmin` declares these at startup. Importantly, the *application* usually decides *when* a message is unrecoverable — by having the listener throw an exception that the container translates into a reject-without-requeue (e.g. `AmqpRejectAndDontRequeueException`), which then triggers the broker's dead-lettering. **Gotcha:** Declaring `x-dead-letter-exchange` does nothing by itself unless something actually rejects-without-requeue, sets a TTL, or a length limit is hit. Many candidates think adding the argument alone routes failures — it must be paired with the right rejection behavior. Also, the DLX and DLQ must exist and be bound, or dead-lettered messages are silently dropped (unroutable).

  • You added x-dead-letter-exchange but messages never appear in the DLQ. Name two likely causes.
    Either nothing is actually rejecting with requeue=false (the listener requeues instead, e.g. defaultRequeueRejected=true and no AmqpRejectAndDontRequeueException), or the DLX has no binding to the DLQ so republished messages are unroutable and dropped.
  • Can you change a queue's dead-letter exchange after it's created?
    No — queue arguments are fixed at declaration. You must delete and redeclare the queue (or use a new queue), because RabbitMQ rejects a redeclare with different arguments as a channel-level PRECONDITION_FAILED error.

context

open as a page

Explain nack-requeue vs reject-without-requeue in a Spring AMQP listener, and how the wrong choice creates a poison-message loop.

level: middleimportance: must knowfreq 75%

basics

~20 s

Nack with requeue=true puts the message back on the queue to be redelivered; reject (requeue=false) discards or dead-letters it. If a message always fails and you keep requeuing it, it's redelivered forever — a poison-message loop. Reject-without-requeue breaks the loop.

open as a page

What is RepublishMessageRecoverer and why prefer it over the default RejectAndDontRequeueRecoverer?

level: seniorimportance: should knowfreq 60%

basics

~20 s

It's a MessageRecoverer that, after retries are exhausted, republishes the failed message to a dead-letter exchange/queue and adds headers with the exception message and stack trace. Unlike the default recoverer, you get diagnostic context in the DLQ.

open as a page

Compare stateful vs stateless retry interceptors in Spring AMQP. When must you use stateful retry?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Stateless retry loops in memory on the same delivery without going back to the broker between attempts. Stateful retry rejects and lets the broker redeliver, tracking attempts by a message key across redeliveries. Use stateful when each attempt needs a fresh transaction/redelivery.

open as a page

Design a production retry-and-dead-letter topology that gives delayed (backoff) retries without blocking consumer threads, and caps attempts before parking to a DLQ.

level: principalimportance: should knowfreq 40%

basics

~20 s

Send failures to a 'wait' queue that has a TTL and no consumer; its dead-letter exchange routes expired messages back to the main queue after the delay. Count redeliveries via the x-death header and, once a cap is hit, route to a terminal parking DLQ instead of retrying.

open as a page