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.

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
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
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.dead1/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
Quick check
What's the main job of a dead letter queue?
Go deeper
- 1Configure a dead letter queue for a job queue and trigger a message to land there.
- 2Build a simple process for reviewing and reprocessing dead-lettered messages.
- 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.