Skip to content

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.