Dead Letter Queues¶
Why Do Dead Letter Queues Exist?¶
At-least-once delivery means a message that fails to process gets redelivered — that's the whole point, don't give up until someone confirms it's handled. But that raises an awkward question: what if a message can never succeed, no matter how many times it comes back?
Message: { "user_id": "not_a_number", "amount": 500 }
↓
Consumer tries to process it → crashes parsing "not_a_number" as an integer
↓
Broker: no ack received → REDELIVER
↓
Consumer tries again → crashes again, same reason
↓
Broker: REDELIVER
↓
... forever, since the data itself is malformed and no retry will ever fix that
That's a poison pill — a message that will never succeed, because the problem isn't transient (a slow database, a network blip) but permanent (bad data, a bug a specific message shape triggers). Left unhandled, a poison pill gets redelivered forever, burning processing resources and — worse — potentially blocking every message behind it in a queue or partition that needs ordered processing.
A dead letter queue is where a message goes after failing a set number of times, instead of getting retried forever. It's an escape hatch: stop retrying automatically, let a human or a separate process take a look.
Normal Queue Dead Letter Queue
Message fails → retry 1 (empty, initially)
Message fails → retry 2
Message fails → retry 3 (max reached)
↓
Message is moved to the DLQ instead of retried a 4th time
What Problem It Actually Solves¶
- Prevents infinite redelivery loops — a poison pill stops burning resources once it's moved aside.
- Prevents head-of-line blocking — in a system where ordering matters (a Kafka partition, a FIFO queue's message group), one permanently-failing message at the front can block everything behind it. A DLQ removes the blocker.
- Preserves failed messages for investigation — instead of a message that could never be processed just vanishing, it sits somewhere visible so someone can actually look at it.
- Separates transient failures from permanent ones — a DLQ is the concrete mechanism that draws that line, once enough retries have failed to make a failure look like more than bad luck.
How It Works¶
Queue configuration: maxReceiveCount = 3
deadLetterTargetArn = arn:aws:sqs:...:orders-dlq
↓
Message received and fails to process → receive count = 1
Message redelivered, fails again → receive count = 2
Message redelivered, fails again → receive count = 3 (limit reached)
↓
Instead of a 4th redelivery attempt, the message is moved to the DLQ
# SQS example
sqs.create_queue(
QueueName="orders",
Attributes={
"RedrivePolicy": json.dumps({
"deadLetterTargetArn": dlq_arn,
"maxReceiveCount": "3",
})
},
)
# Kafka doesn't have a native DLQ primitive the way SQS does — it's
# implemented explicitly in consumer code, publishing to a separate topic
def process_message(message):
try:
handle(message)
except PermanentFailure:
producer.send("orders-dlq", value=message.value, headers=message.headers)
except TransientFailure:
raise # let normal retry/redelivery handle it
The mechanism differs by system. SQS and most managed queues ship a native maxReceiveCount-based redrive policy. Kafka has nothing built in — a DLQ there means hand-writing a consumer that catches a permanent failure and republishes to a separate topic after exhausting its own retry logic.
What Happens to a Message in the DLQ¶
A DLQ isn't a place messages go to be forgotten. Without an operational story behind it, it's just a slower version of silently dropping data.
Message lands in DLQ
↓
Options:
1. Alert an engineer / on-call — "3 messages landed in orders-dlq, investigate"
2. Automated inspection — a separate process reads the DLQ, logs details,
maybe categorizes common failure types
3. Manual fix and redrive — once the underlying bug or bad data is fixed,
move the message BACK to the original queue for reprocessing
4. Permanent discard — after investigation, some messages genuinely can't
be recovered (the data was simply wrong) and get discarded deliberately,
which is different from silently dropping them without ever looking
There's a real difference between discarding a message after someone has actually looked at it, and a poison pill quietly vanishing because nobody was watching an unmonitored DLQ. A DLQ with no monitoring attached is barely better than having none — it just delays the moment the data loss becomes anyone's problem.
Real-World Examples¶
AWS SQS Redrive Policy¶
The maxReceiveCount + deadLetterTargetArn setup above is a built-in SQS feature — no custom consumer logic needed, just queue configuration.
Kafka Consumer-Side DLQ Topics¶
Since Kafka has nothing native for this, consumers typically catch a permanent failure, publish the failed message (often with added metadata — the error, the timestamp, attempt count) to a separate -dlq topic, and only then commit the original offset so it stops being redelivered.
Payment Processing Pipelines¶
A textbook case for taking DLQs seriously — a malformed payment event should never quietly disappear after exhausting retries. It needs to land somewhere visible and trigger an alert, because "we silently lost track of a payment" isn't an acceptable outcome, ever.
Common Interview Questions¶
What is a poison pill, and why is it a problem for at-least-once delivery?¶
A message that can never process successfully, no matter how many retries, because the failure is permanent (malformed data, a bug a specific message shape triggers) rather than transient. Without a DLQ, at-least-once delivery's redeliver-until-acknowledged design retries it forever — wasting resources and potentially blocking everything behind it in an ordered queue or partition.
How does a dead letter queue actually get triggered?¶
A queue or consumer tracks how many times a message has failed. Once that hits a configured threshold — SQS's maxReceiveCount, or equivalent logic in a hand-rolled Kafka consumer — the message moves to a separate dead letter queue instead of getting redelivered again.
Why does a DLQ matter specifically for systems that require ordered processing?¶
A permanently-failing message at the front of an ordered stream — a Kafka partition, a FIFO queue's message group — blocks everything behind it, since order has to be preserved. Moving the poison pill to a DLQ is what unblocks the rest of the queue.
What should actually happen to messages once they land in a DLQ?¶
Real operational follow-through — alerting, monitoring, or automated inspection, not passive storage. Messages get fixed and redriven back for reprocessing, or deliberately discarded after someone's looked at them. A DLQ nobody watches just delays when the data loss gets noticed, it doesn't prevent it.
Does Kafka have a native dead letter queue feature?¶
No. Unlike SQS's built-in redrive policy, Kafka has nothing first-class for this. It's implemented in consumer code: catch the permanent failure, publish to a separate DLQ topic, then commit the original offset so the source topic stops redelivering it.
Common Mistakes¶
❌ Setting up a DLQ with no monitoring or alerting attached.
Just delays when data loss gets noticed — it isn't a fix on its own, only the mechanism that makes noticing possible.
❌ Setting maxReceiveCount (or an equivalent retry limit) too low.
A transient failure that would have succeeded on retry 4 gets prematurely routed to the DLQ instead, treated as permanent when it wasn't.
❌ Setting the retry limit too high, or with no backoff between attempts.
Wastes resources hammering a genuine poison pill, delaying the point where it's correctly recognized as unrecoverable and moved aside — worth pairing with the backoff strategies in the next chapter.
❌ Treating the DLQ as a place messages simply disappear to.
Without a deliberate decision to redrive, fix, or discard, a DLQ quietly accumulating failures isn't much different from not handling failures at all.
Key Takeaways¶
- A dead letter queue exists because at-least-once delivery's retry-until-acknowledged design has no natural stopping point for a message that can genuinely never succeed — a poison pill — without one.
- Moving a message to a DLQ after a bounded number of failures prevents both endless reprocessing and head-of-line blocking in systems that need ordered processing.
- SQS supports DLQs natively via a redrive policy; Kafka has nothing built in, so the pattern gets hand-implemented as a separate topic.
- A DLQ only earns its keep if something actually happens to what lands in it — alerting, investigation, a deliberate redrive/fix/discard decision — not passive, unmonitored storage.
- The retry threshold needs deliberate tuning: too low prematurely dead-letters recoverable failures; too high wastes resources on a genuine poison pill before recognizing it as unrecoverable.