Skip to content

Dead-Letter Exchanges

The problem: a message you can’t process

Section titled “The problem: a message you can’t process”

A consumer pulls a message, tries to process it, and fails — the payload is malformed, a downstream service is down, or the record it references doesn’t exist. What now?

If you nack it with requeue, RabbitMQ puts it right back at the front of the queue, you pick it up again immediately, it fails again, and you have a poison message spinning in a tight loop, burning CPU and blocking everything behind it. If you just drop it, you’ve silently lost data. Neither is acceptable.

The answer is to send the failed message somewhere else on purpose. That “somewhere else” is a dead-letter exchange (DLX).

A message is dead-lettered — routed to the queue’s configured DLX — when any of these happen:

  • It is rejected or nacked with requeue=false (your consumer says “don’t give this back to me”).
  • Its message TTL expires while sitting in the queue.
  • The queue hits its max-length limit and the message is dropped to make room (overflow).

You attach a DLX to a queue with the x-dead-letter-exchange argument (and optionally x-dead-letter-routing-key to relabel it on the way out).

flowchart LR
  q["work queue
(x-dead-letter-exchange: dlx)"] -->|"nack requeue=false
or TTL expiry
or overflow"| dlx["DLX (exchange)"]
  dlx --> dlq["dead-letter queue"]
  dlq --> ops["ops / inspector
/ retry logic"]
A failed message flows to the dead-letter queue
await channel.assertExchange('dlx', 'fanout', { durable: true });
await channel.assertQueue('orders.dead', { durable: true });
await channel.bindQueue('orders.dead', 'dlx', '');
// the work queue dead-letters to the DLX
await channel.assertQueue('orders', {
durable: true,
arguments: { 'x-dead-letter-exchange': 'dlx' },
});
// in the consumer, reject a bad message WITHOUT requeue → it dead-letters
channel.nack(msg, false, false);

Not everything should be retried forever. A message that will never succeed — bad schema, a reference that no longer exists — is a poison message. The pattern is: track how many times a message has been tried (a counter in a header, or the x-death records RabbitMQ adds on each dead-letter), and after N attempts, route it to a parking (or “dead”) queue where a human or an alert can look at it. Never let a poison message loop.

Here is the clever trick that dead-lettering unlocks. Sometimes a failure is transient — a downstream service is briefly down — and you want to retry, but not immediately. You want to wait 30 seconds and try again.

Combine a message/queue TTL with a DLX:

  1. On failure, publish the message to a retry queue that has a TTL of 30s and whose DLX points back at the main exchange.
  2. The message sits in the retry queue doing nothing for 30 seconds.
  3. When the TTL expires, RabbitMQ dead-letters it — which routes it back to the main queue for another attempt.

You’ve built a delayed retry with backoff using nothing but queue configuration — no scheduler, no sleep in your consumer. (The Consumer Patterns module builds this into a full retry ladder.)

Why is nacking a failed message with requeue=true dangerous?
Which of these does NOT dead-letter a message?
How do you handle a poison message that will never succeed?
How does TTL + DLX create a delayed retry?