01 · Definition
What is a dead-letter queue (DLQ)?
A dead-letter queue (DLQ) is a separate queue where a messaging system puts the messages it has given up on: jobs that failed every retry, payloads that failed validation, or messages that expired or exceeded a delivery limit. Moving them aside keeps the main Message Queue flowing — a broken message no longer sits at the head blocking workers or looping through retries — while keeping the message itself, so nothing is silently lost.
Most brokers support it natively. A queue can carry a redrive policy (move a message after N receives) or a dead-letter exchange (route messages that are rejected without requeue, expire or overflow), and job frameworks keep a failed set with the last error. In a WhatsApp pipeline the typical residents are sends that failed with permanent errors such as Error 132000 (Parameter Count Mismatch) or 132001 (template does not exist), sends whose retry budget ran out during a long throughput incident, and webhook events your code could not parse.
The DLQ is the parking lot; a Poison Message is the kind of payload that typically ends up there, failing its consumer every time. A DLQ is only useful with a process around it: an alert when it fills, enough context stored with each message to diagnose it, retention long enough to act, and a redrive path that sends fixed messages back to the main queue — at a controlled rate and only into consumers protected by an Idempotency Key, so replays do not double-send.
Key takeaways
- A dead-letter queue holds messages that cannot be processed — retries exhausted or permanently invalid — so they stop blocking the main queue.
- Send permanent errors straight to the DLQ: a 132000 parameter mismatch will fail on every retry.
- Store the original payload with the error code, attempt count, timestamps and trace ID, or triage becomes guesswork.
- Alert when DLQ depth rises above zero, give every DLQ an owner, and keep messages longer than your triage takes.
- Redrive only after fixing the cause, at a controlled rate, into idempotent consumers so replays cannot double-send.
02 · Specifications
A dead-letter queue (DLQ): key facts and limits
| Purpose | Park unprocessable messages without blocking the main queue |
|---|---|
| Typical delivery limit | 3–10 attempts (illustrative) |
| Dead-letter at once | 132000, 132001, 131051, malformed payloads |
| Retry first | Timeouts, HTTP 5xx, 130429, 131000 |
| Retention | Longer than the source queue and your triage time |
| Alert | Depth above zero, or a sudden rise |
| Redrive | After the fix, rate-limited, into idempotent consumers |
03 · Code samples
A dead-letter queue (DLQ) in code
import amqp from "amqplib";const conn = await amqp.connect(process.env.AMQP_URL);const ch = await conn.createChannel();// The dead-letter exchange and the DLQ it routes toawait ch.assertExchange("wa.dlx", "direct", { durable: true });await ch.assertQueue("wa.send.dlq", { durable: true });await ch.bindQueue("wa.send.dlq", "wa.dlx", "wa.send");// Main queue: messages over the delivery limit are dead-lettered to wa.dlxawait ch.assertQueue("wa.send", {durable: true,deadLetterExchange: "wa.dlx",deadLetterRoutingKey: "wa.send",arguments: { "x-queue-type": "quorum", "x-delivery-limit": 5 },});const PERMANENT = new Set([100, 131051, 132000, 132001]); // fail identically on every retrych.consume("wa.send", async (msg) => {try {await sendTemplate(JSON.parse(msg.content.toString()));ch.ack(msg);} catch (err) {if (PERMANENT.has(err.code)) {// Dead-letter now, with the context triage needs, then ack the originalch.publish("wa.dlx", "wa.send", msg.content, {persistent: true,headers: { "x-error-code": err.code, "x-error": err.message, "x-failed-at": new Date().toISOString() },});ch.ack(msg);} else {ch.nack(msg, false, true); // transient: requeue (add a delay in production);} // after 5 deliveries the broker dead-letters it}});
Permanent errors are dead-lettered on the first failure with context; transient ones retry until the delivery limit moves them.
04Playground
A working model of DLQ. Use its controls to replay it step by step.
Follow a failing send into the DLQ and back
- Order service to wa.send: enqueue
- wa.send to Send worker: consume
- Send worker to Cloud API: send
- Send worker to Retry with backoff: 5xx / timeout
- Retry with backoff to wa.send: attempt + 1
- Send worker to Dead-letter queue: 5th failure or 132000
- Dead-letter queue to Alert & triage: depth > 0 alert
- Alert & triage to Fix & redrive: fix template
- Fix & redrive to wa.send: redrive
Hop 11 of 11
Send workerCloud APIsend
Arrives at Cloud API — POST /messages.
Hop 11 of 11: Send worker to Cloud API — send.
05 · Operation
How does a dead-letter queue (DLQ) work?
Define what counts as dead
Permanent errors — invalid parameters, a missing template, an unsupported message type — go to the DLQ at once. Transient errors such as timeouts, 5xx and 130429 retry under your Retry Policy with exponential backoff and Backoff Jitter, and reach the DLQ only when the retry budget is spent.
Wire the DLQ into the broker
Configure a delivery limit or redrive policy (for example 5 attempts) and a dead-letter target on the main queue, so the broker moves messages even if a worker crashes mid-job.
Store context with each message
Keep the original payload plus the last error code and message, attempt count, first and last failure time, worker version and trace ID.
Alert, then triage by error code
Wire the DLQ into your alerting: fire when its depth rises above zero or grows fast. Group entries by error code and template name to find the single bug behind hundreds of failures.
Fix, then redrive
Correct the template, data or code, then move messages back to the main queue at a limited rate. Discard, with a record, those that should no longer be sent — an OTP that expired days ago, say.
06 · Cautions
Common mistakes with a dead-letter queue (DLQ)
Retrying permanent errors
A 132000 or a missing template fails identically on every attempt; retrying five times wastes throughput and delays the alert. Classify errors and dead-letter permanent ones on the first failure.
A DLQ nobody watches
Messages in an unmonitored DLQ are lost with extra steps. Alert on depth, assign an owner and review it in a regular triage.
Retention shorter than triage
If the DLQ keeps messages for four days and the team triages weekly, failures expire unseen. On some brokers a message keeps its original enqueue time when dead-lettered, so set DLQ retention well beyond the source queue's.
Redriving everything at once
Replaying 50,000 messages in one burst hits the number's throughput, triggers 130429s and can send stale content. Redrive at a controlled rate and filter out messages that are no longer relevant.
Stripping the context
A DLQ entry with only the payload forces developers to dig through logs. Attach the error code, attempt count, timestamps, worker version and trace ID when dead-lettering.
07 · Worked example: Out-for-delivery updates at a Nagpur courier
Example: Out-for-delivery updates at a Nagpur courier
A courier company in Nagpur sends out-for-delivery utility templates from a queue. After a template edit removes a variable, every send for that template fails with error 132000. The workers dead-letter these on the first failure instead of retrying, and the DLQ-depth alert opens a ticket within five minutes. The DLQ groups 1,420 messages under one error code and one template name; the developer restores the variable, the team discards messages for parcels already delivered, and redrives the remaining 1,100 at 20 per second. Customers get their updates about 40 minutes late instead of not at all.
08 · Rationale
Why does a dead-letter queue (DLQ) matter?
Without a DLQ, failed messages either loop forever, burning workers and throughput, or get dropped — and someone learns weeks later that 3,000 delivery updates never went out. With one, failures become a visible, bounded worklist: support can see which customers were not notified, developers get the exact payload and Meta error code to reproduce the bug, and once it is fixed the messages can be replayed instead of rebuilt by hand.
09 · Recommended practice
Best practices for a dead-letter queue (DLQ)
- Keep one DLQ per main queue, so ownership and the redrive target are obvious.
- Build a redrive tool with dry-run, filtering by error code and a rate limit before you need it at 2 am.
- Chart DLQ arrivals per error code; a new code appearing is often the first sign of a template or API change.
- Never redrive time-bound messages such as OTPs; record them as failed and let the user request a new one.
10 · Questions and answers
DLQ FAQs
- What is the difference between a dead-letter queue and a retry queue?
- A retry queue holds messages that will be tried again automatically after a delay; a dead-letter queue holds messages the system has stopped retrying. Messages move to the DLQ when retries are exhausted or the error is permanent.
- When should a message go straight to the DLQ?
- When the error is permanent and retrying cannot change the outcome. For WhatsApp sends that includes parameter mismatches (132000), a template that does not exist (132001) and malformed payloads; timeouts and 5xx errors should retry first.
- How do I redrive messages from a DLQ safely?
- Fix the cause first, then replay into the main queue at a controlled rate with idempotent consumers. Filter out messages that are no longer relevant, such as expired OTPs, and watch the main queue's error rate during the redrive.
- How long should messages stay in a DLQ?
- Longer than your slowest realistic triage — typically one to two weeks. Check whether your broker measures expiry from the original enqueue time, and archive anything you must keep for audit before it expires.