Files
spring-messaging-demo/rabbitmq/docs/04-dead-lettering.md
2026-08-29 10:30:21 +05:30

4.3 KiB

← The silent drop · Module README · Acknowledgement →

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.

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:

=== 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

channel.basicNack(deliveryTag, false, true);   // requeue = true

From docs/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:

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 →