Overview
A dead-letter queue (DLQ) is where messages go when they cannot be processed after a set number of attempts, for example because of malformed data, a missing record, or a persistent bug. Without a DLQ, such 'poison messages' are retried forever, wasting resources and blocking other messages.
A DLQ isolates failures so the main queue keeps flowing, while preserving the failed messages for investigation and redrive (reprocessing after a fix). DLQs need monitoring and alerting; a silently growing DLQ is lost work.
Letters that cannot be delivered after several attempts go to a special desk instead of circulating forever. Someone investigates, fixes the address, and resends them.
When to use it
- Every production queue or event consumer.
- Handling poison messages that always fail.
- Preserving failed work for later reprocessing.
- Separating transient failures from permanent ones.
Where it shows up in interviews
Recognize it when: some messages will fail permanently.
- Design a notification system
- Design a payment webhook processor
Where it is used in real software
After maxReceiveCount receives, SQS moves messages to the DLQ; redrive sends them back after a fix.
Rejected, expired, or overflow messages are routed to a DLX.
Kafka Connect and Spring Kafka publish failed records to a separate topic with error headers.
Key terms
- Poison message
- A message that fails every time.
- maxReceiveCount
- Attempts before moving to the DLQ.
- Redrive
- Moving messages from the DLQ back to the source queue.
- Error metadata
- Headers with the failure reason, stack trace, attempt count.
How it works, step by step
- 1Consumer fails a message
Throws or nacks.
- 2Retry with backoff
Transient errors often succeed later.
- 3Exceed the retry limit
Move the message to the DLQ with error details.
- 4Alert and investigate
Alarm on DLQ depth greater than zero.
- 5Fix and redrive
Deploy the fix, then reprocess DLQ messages idempotently.
STEP 1Message 42 has a malformed field; processing throws.
Transient vs permanent failures
Classify errors before retrying
| Error | Type | Action |
|---|---|---|
| Timeout calling a service | Transient | Retry with backoff |
| HTTP 429 or 503 | Transient | Retry with backoff |
| JSON parse error | Permanent | Send to DLQ immediately |
| Referenced record not found | Maybe transient (lag) | A few retries, then DLQ |
| Validation failure (400) | Permanent | DLQ, alert the producer team |
NOWError: Timeout calling a service | Type: Transient | Action: Retry with backoff
Retrying permanent errors wastes capacity; sending them straight to the DLQ keeps the pipeline healthy.
Implementation
const MAX_ATTEMPTS = 5; await consumer.run({ eachMessage: async ({ message }) => { const attempts = Number(message.headers?.attempts?.toString() ?? "0"); try { await handle(JSON.parse(message.value!.toString())); } catch (err) { const permanent = err instanceof SyntaxError || (err as { status?: number }).status === 400; const target = permanent || attempts + 1 >= MAX_ATTEMPTS ? "orders.dlq" : "orders.retry"; await producer.send({ topic: target, messages: [{ key: message.key, value: message.value, headers: { attempts: String(attempts + 1), error: String((err as Error).message).slice(0, 500) }, }], }); } },});Complexity and performance
With exponential backoff.
Time to investigate.
Trade-offs
More retries recover more transient failures but delay detection of permanent ones.
Moving a message to a DLQ lets later messages for the same key proceed, which can break per-key order.
Variants and related techniques
orders.retry.1m, orders.retry.10m before the DLQ.
Manual-review queue for messages needing human action.
Common mistakes
- No alert on DLQ depth.
Fix: Alert when the DLQ has messages; they represent failed business work.
- Redriving before fixing the bug.
Fix: Messages will fail again; fix first.
- Losing error context.
Fix: Store error reason, attempts, and original topic in headers.
Interview questions
What is a dead-letter queue and why use one?
A separate queue for messages that fail after several attempts. It prevents poison messages from blocking or wasting resources, keeps the main flow healthy, and preserves failed messages for debugging and reprocessing.
How do you handle ordering with DLQs?
If per-key order matters, pause processing of that key (or partition) when a message fails permanently, or record the failure and make later events tolerate a missing predecessor.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Configure an SQS DLQ with alarms | Easy | Redrive policy. |
| Design retry topics and a DLQ for Kafka | Medium | Error classification. |