Stop 15 · Message Queues & Event-Driven ArchitectureTopics · 7/8

Dead Letter Queues

Part of the Backend Developer Roadmap

In short

Some messages will never succeed: a bad email address, broken JSON, a deleted user. Without a dead letter queue, they retry forever or vanish silently. Both are bad.

A separate queue where messages that repeatedly fail processing get routed instead of being retried forever or silently dropped — lets you inspect and fix failures without losing data.

The blob as a postman places a crumpled envelope into a separate red tray on a shelf.

Think of it like this

The post office's undelivered tray

The postman tries a house three times. Nobody opens. He doesn't keep circling the street forever, and he doesn't throw the letter away. It goes into a separate tray, where someone checks the address and decides what to do. That tray is your dead letter queue.

Watch it happen

A poison message's last stop

Main queue
Worker
Retries
DLQ
Fix, replay

1/5A message with userId 999 arrives. That user was deleted yesterday.

The big ideas

What you need to know

Poison messages

A message that fails every time can block a queue or burn CPU forever. Cap retries, then move it aside.

Alert on DLQ size

A DLQ nobody watches is a silent bin. Alert when it grows, so failures get looked at.

Keep the reason

Store the error message and attempt count with the dead message, so debugging doesn't start from zero.

BullMQ's version

In BullMQ, jobs that use up their attempts land in the failed set. Treat it like a DLQ and review it.

See it in code

Walk through the code

ts
1await ch.assertExchange("dlx", "direct", { durable: true });2await ch.assertQueue("emails.dead", { durable: true });3await ch.bindQueue("emails.dead", "dlx", "emails");4 5await ch.assertQueue("emails", {6  durable: true,7  deadLetterExchange: "dlx",8  deadLetterRoutingKey: "emails",9});10 11// In the consumer, give up on a poison message:12ch.nack(msg, false, false); // no requeue, so it goes to emails.dead

1/3Create a dead-letter exchange and a queue to collect the failures, bound with the key emails.

RabbitMQ classic queues don't count retries for you; track attempts in a header or use quorum queues with a delivery limit.

Save yourself the pain

Common mistakes

Oops

Requeueing failed messages forever with nack(msg, false, true).

Do this instead

A poison message loops endlessly. Cap attempts, then dead-letter it.

Oops

Adding DLQ settings to an existing RabbitMQ queue with assertQueue.

Do this instead

Changed arguments cause a PRECONDITION_FAILED error. Use a policy, or create a new queue.

Your mission

Catch a poison message

~30 min

Quick check

What's the main job of a dead letter queue?

Go deeper

  1. 1Configure a dead letter queue for a job queue and trigger a message to land there.
  2. 2Build a simple process for reviewing and reprocessing dead-lettered messages.
  3. 3Learn why silently dropping failed messages is a common, costly mistake.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge