SysDesignPrep.com
Interactive concepts

Dead-letter queues and poison messages: an interactive simulator

Some messages can never be processed: malformed payloads, references to deleted records, a bug triggered by one customer’s data. These poison messages fail on every attempt. Retrying them forever blocks or slows everything else, so queues move a message to a dead-letter queue (DLQ) after a few failed attempts, where it waits for a person or a tool to inspect it, fix the cause and replay it.

The playground runs a consumer for ten minutes against a queue where a small share of messages are poison. Compare retrying in place, requeueing forever, and a dead-letter queue.

Messages arrive at 6 a second and take 100 ms each; 2% of them are poison, malformed in a way that makes the handler fail every time, after a 0.5 s timeout. Retrying in place, the consumer meets the first poison message after 3 seconds and never gets past it: only 14 messages are handled in ten minutes and 3,525 are waiting at the end. Sending failures to the back of the queue keeps things moving, but poison messages never leave: 78 are circling by the end, each costing another timeout on every lap, and good messages wait 35.4 s at the median. Moving a message to a dead-letter queue after 3 attempts takes the 78 poison messages out of the way, where someone can inspect and replay them, and good messages wait 0.3 s at the median.

Messages waiting in the queue, over ten minutes
1101001,0000 min2 min4 min6 min8 min10 min
On failureHandledMedian waitp99 waitPoison set aside
Retry in placekeep retrying the message at the head144.8 min9.8 minnone; 78 still queued
Requeue foreversend it to the back and try again later3,04435.4 s1.2 minnone; 78 still queued
Dead-letter queue3 attempts, then set it aside3,4690.3 s3.9 s78

Two ways poison hurts

  • Head-of-line blocking. A consumer that must process messages in order, such as a Kafka partition consumer, keeps retrying the message at the head. With 2% poison, the first one arrives within seconds and nothing behind it is processed again: 14 messages handled in ten minutes, 3,500 waiting.
  • Endless recirculation. Requeueing failures at the back keeps good messages flowing, but poison never leaves. By the end, 78 poison messages are circling, each costing a timeout on every lap, and good messages wait over half a minute. Given longer, they would use up the consumer entirely.

A dead-letter queue bounds the damage: after 3 attempts a message is set aside, the consumer moves on, and good messages wait well under a second. The 78 poison messages are not lost; they sit in the DLQ with their error, for someone to fix and replay.

Running a dead-letter queue well

  • Separate transient from permanent failures. A timeout talking to a database deserves retries with backoff, ideally on a delay queue so they do not block others; a schema violation should go to the DLQ at once.
  • Alert on DLQ depth. A DLQ nobody watches is a slow data-loss bug. Page or ticket when messages land, and keep the error and attempt count with each one.
  • Make replay safe. Replayed messages arrive late and out of order, so handlers must be idempotent and tolerate stale data.
  • Mind ordering. Skipping a message breaks order for its key; if order matters, park the whole key until the poison message is resolved.

A dead-letter queue in SQS and Kafka

# SQS: after 3 failed receives, the message moves to the dead-letter queue
aws sqs set-queue-attributes --queue-url $ORDERS_QUEUE --attributes '{
  "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:eu-west-1:123456789012:orders-dlq\",\"maxReceiveCount\":\"3\"}"
}'

# Kafka has no built-in DLQ; consumers publish failures to their own topic
def consume(msg):
    try:
        handle(msg)
    except PermanentError:                   # bad data: retrying will not help
        dlq.send("orders.dlq", msg.value, headers={"error": traceback.format_exc()})
    except TransientError:
        retry_topic.send("orders.retry-1m", msg.value)   # retry later, off the main path
    consumer.commit(msg)                     # either way, the partition moves on

In a system design interview

Whenever you put a queue in a design, say what happens to a message that keeps failing: “Consumers retry transient errors three times with backoff through a delay topic. Anything still failing, and anything that fails validation, goes to a dead-letter topic with the error attached. DLQ depth pages the owning team, and a replay tool re-publishes fixed messages to the main topic; handlers are idempotent, so replays are safe.” Expect the follow-ups: ordering for the affected key, how long DLQ messages are kept, and alerting.

Read the guide to message queues and streams or background jobs, see message ordering and delivery semantics, or watch a strong candidate design a message queue.

Questions people ask

What is a dead-letter queue?

A separate queue where messages are moved after they have failed processing a set number of times. It keeps failing messages from blocking or slowing the main queue while preserving them, with their error, so they can be inspected, fixed and replayed.

What is a poison message?

A message that fails every time it is processed, usually because of malformed data, a reference to something that no longer exists, or a bug triggered by its contents. Retrying it cannot succeed until the data or the code is fixed.

How does a poison message block a queue?

An ordered consumer, such as one reading a Kafka partition, cannot move past a message until it succeeds. If it keeps retrying a poison message, every message behind it waits indefinitely: head-of-line blocking.

Why not just requeue failed messages?

Requeueing to the back keeps other messages moving, but a poison message never succeeds, so it circulates forever and costs another failed attempt on every pass. As more poison accumulates, it consumes more and more of the consumer’s capacity.

How many retries before sending a message to the DLQ?

Usually a few, three to five, with backoff between them, so transient failures such as timeouts get a chance to succeed. Errors known to be permanent, like validation failures, can go to the DLQ immediately.

How do you replay messages from a dead-letter queue?

After fixing the cause, a tool moves the messages back to the main queue or topic, as SQS redrive does. Handlers must be idempotent, because replayed messages may have been partly processed before and arrive out of order.

Does Kafka have dead-letter queues?

Not built into the broker. Consumers or frameworks such as Kafka Connect and Spring Kafka publish failed records to a dead-letter topic, often after passing them through delayed retry topics, and commit the original offset so the partition keeps moving.