[← The silent drop](03-the-silent-drop.md) · [Module README](../README.md) · [Acknowledgement →](05-acknowledgement.md) # 4. A dead-letter queue that actually works There is no "send to DLQ" operation in AMQP. A dead-letter exchange is a **queue argument**, and messages arrive there as a side effect of three specific events. ```java QueueBuilder.durable("orders.work") .deadLetterExchange("orders.dlx") .deadLetterRoutingKey("failed") .build(); ``` `deadLetterRoutingKey` matters more than it looks. Without it, the message is republished with its **original** routing key, so your DLQ binding has to anticipate every routing key the upstream might have used. Setting it to a constant means one binding catches everything. ## The three triggers | Event | `x-death` reason | |---|---| | `basicNack` / `basicReject` with `requeue=false` | `rejected` | | message TTL expires (`x-message-ttl` or per-message) | `expired` | | queue length or byte limit exceeded (`x-max-length`) | `maxlen` | And the one that is **not** a trigger: `requeue=true`. More on that below. ## What arrives Both captured from real runs, in [`docs/output/dead-letter.txt`](output/dead-letter.txt): ``` === x-death after basicNack(requeue=false) === reason rejected count 1 exchange time Sat Aug 29 09:53:17 IST 2026 routing-keys [orders.work] queue orders.work === x-death after x-message-ttl expiry === reason expired count 1 queue orders.ttl ``` Same destination queue, different `reason`. That field is the whole diagnostic: `rejected` means a consumer looked at the work and refused it; `expired` means nobody got to it in time. Those are different incidents with different fixes, and they are indistinguishable without reading the header. The third trigger has a detail worth knowing: when `x-max-length` is exceeded RabbitMQ drops from the **head**, so the message that gets dead-lettered is the **oldest** one already queued, not the one that just arrived: ``` === x-death after x-max-length overflow === reason maxlen queue orders.bounded body {"orderId":"m-1","detail":"detail for m-1"} ``` Three messages into a queue that holds two, and `m-1` is the one on the DLQ. A bounded queue under sustained overload therefore dead-letters your *backlog* while continuing to accept new work — which is usually what you want for telemetry and exactly wrong for orders. `x-death` is a **list**, not a map — one entry per queue the message has been dead-lettered from, and `count` accumulates. That is how you build a retry limit: read `x-death[0].count`, and stop republishing past a threshold. ## The infinite loop ```java channel.basicNack(deliveryTag, false, true); // requeue = true ``` From [`docs/output/requeue-loop.txt`](output/requeue-loop.txt), 200 delivery attempts: ``` redelivered 199 times dead-lettered 0 still on queue 1 ``` The dead-letter exchange is never consulted. `x-death` is never written. There is no counter to exhaust and no backoff. One poison message with an unconditional `requeue=true` handler will saturate a consumer indefinitely, and the queue depth stays at 1 the whole time — so a depth alarm will not fire either. The only visible signal is CPU. **`requeue=true` is only correct when the failure is transient and you have a delay.** With no delay, a message that fails because a database is down comes straight back while the database is still down. The `redelivered` flag on the envelope is your one piece of state: ```java boolean retried = response.getEnvelope().isRedeliver(); channel.basicNack(tag, false, !retried); // one requeue, then dead-letter ``` That is a one-attempt retry with no extra infrastructure. For anything more, use a delay queue: a queue with a TTL and a DLX pointing back at the work exchange turns "expire after 30 seconds" into "retry in 30 seconds". ## Do not point the DLQ's DLX at its own source If `orders.dlq` dead-letters to an exchange bound back to `orders.work`, a message that fails consistently cycles between the two forever, gaining an `x-death` entry each time until the header itself is the biggest thing in the message. The DLQ in this module deliberately has no dead-letter exchange at all. [Acknowledgement →](05-acknowledgement.md)