Dead-Letter Queues: Handling Messages That Won't Process
A practical guide to dead-letter queues, the pattern that isolates poison messages so one bad payload can't stall your whole pipeline.
Key takeaways
A practical guide to dead-letter queues, the pattern that isolates poison messages so one bad payload can't stall your whole pipeline.
On this page
Every queue-based system eventually meets a message it cannot process. The JSON is malformed, a referenced record was deleted, a downstream API returns a permanent 400, or the consumer hits a bug on one specific payload. Without a plan, that single message becomes a wall. It gets picked up, fails, returns to the queue, gets picked up again, and fails again. Meanwhile everything behind it waits. This is the problem a dead-letter queue solves.
What a dead-letter queue is#
A dead-letter queue (DLQ) is a separate queue where messages land after they repeatedly fail to process on the main queue. It is not a special technology. It is an ordinary queue with a specific job: to hold the messages your system gave up on, so they can be inspected and dealt with later instead of being lost or retried forever.
The value is separation. Your primary queue stays reserved for messages that have a reasonable chance of succeeding. The problem cases move somewhere else where they can't do damage. DLQs are a core building block of event-driven architecture, because async systems have no caller waiting on the line to notice a failure and react. The queue has to handle failure on its own.
Why you need one#
Three things go wrong without a DLQ.
Poison messages block the queue: A message that always fails but keeps returning to the front of the queue can stall processing for everything behind it, especially with strict ordering. One bad payload becomes a full outage.
Infinite retry loops burn resources: A consumer that retries a permanently broken message forever wastes compute, floods logs, and can hammer a downstream service into further trouble. Retries should be bounded.
Dropped messages destroy evidence: The lazy fix is to catch the error and discard the message. Now the failure is invisible. You have no record of what broke, no payload to reproduce it, and no way to recover the data. A DLQ preserves the message so you can find out what happened.
What triggers a DLQ#
A message moves to the DLQ when one of these conditions is met:
- Max receive count reached: the message was delivered and returned to the queue more times than the configured limit.
- TTL expiry: the message sat in the queue longer than its time-to-live allows.
- Routing failure: the broker cannot deliver the message to any queue, for example a rejected or unroutable message in RabbitMQ.
- Message too large or rejected: the consumer explicitly rejects it, or it violates a size or schema constraint.
The retry-then-DLQ flow#
The pattern is retry a bounded number of times, back off between attempts, then dead-letter. Exponential backoff spaces retries out so a transient problem, a brief network blip or a downstream restart, has time to clear. First retry after 1 second, then 2, then 4, then 8. If the message still fails after the last attempt, it goes to the DLQ rather than looping again.
The distinction that matters is transient versus permanent failure. Backoff and retry are for transient errors that might resolve on their own. A malformed payload will never parse no matter how long you wait, so for known-permanent errors it is often better to dead-letter immediately rather than waste the retry budget. This connects to broader message queue patterns around delivery guarantees and idempotency.
How it works in real systems#
Amazon SQS uses a redrive policy. You attach the DLQ to the source queue and set maxReceiveCount. Once a message has been received that many times without a successful delete, SQS moves it automatically.
{
"RedrivePolicy": {
"deadLetterTargetArn": "arn:aws:sqs:us-east-1:111122223333:orders-dlq",
"maxReceiveCount": 5
}
}
RabbitMQ uses a dead-letter exchange (DLX). You declare a queue with a DLX, and rejected, expired, or overflowed messages are republished there.
{
"x-dead-letter-exchange": "dlx.orders",
"x-dead-letter-routing-key": "orders.failed",
"x-message-ttl": 60000
}
Kafka has no built-in DLQ, so the convention is an error topic. The consumer catches a processing failure and produces the original message, plus failure metadata, to a dedicated orders.DLT topic. Frameworks like Spring Kafka wire this up with a DeadLetterPublishingRecoverer after a configured number of retries.
What to do with DLQ messages#
A DLQ is a starting point, not a graveyard. The workflow is alert, inspect, fix, redrive.
Alert: any message arriving in the DLQ should trigger a notification. A non-empty DLQ means something is failing that you have not handled.
Inspect: read the payload and the attached error to understand the cause. Is it one poison message or a systemic bug hitting many?
Fix: correct the root cause, whether that is a consumer deploy, a downstream fix, or a data correction.
Redrive or replay: once fixed, move the messages back to the main queue for reprocessing. SQS has a native redrive action; RabbitMQ and Kafka usually need a small script or a shovel to move messages back.
Best practices#
Monitor DLQ depth: treat the number of messages in the DLQ as a first-class metric. It should normally be zero.
Include failure metadata: attach the exception, a timestamp, the retry count, and the source queue to each dead-lettered message. Debugging a bare payload with no context is slow.
Set alarms, not dashboards: an alarm on DLQ depth greater than zero (or above a small threshold) reaches you. A dashboard you have to remember to check does not.
Give the DLQ its own retention and access: keep messages long enough to investigate, and make sure the team can actually read and redrive them.
The call we'd make#
Add a DLQ to any queue whose messages matter, from day one. It costs almost nothing to configure and it is the difference between a contained failure and a stalled pipeline you discover from customer complaints. Set maxReceiveCount to a small number like 5, attach the DLQ, alarm on depth greater than zero, and make sure failure metadata travels with the message. Then decide deliberately which errors deserve retries and which should be dead-lettered on the first failure. The DLQ is not where messages go to die. It is where they wait for you to notice, and that visibility is the whole point.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Pub/Sub Explained: How Publish-Subscribe Messaging Works
A practical guide to publish-subscribe messaging, covering topics, fan-out, delivery guarantees, durable subscriptions, and the real systems that power it.
When to Use a Message Queue (and When Not To)
A practical guide to the signals that justify a message queue, the costs it adds, and a checklist for deciding.
More from DevOps
Explore more articles in this category
Best Managed Kubernetes in 2026: EKS vs GKE vs AKS vs DOKS
The control plane fee is the least interesting number. What separates managed Kubernetes providers is upgrade cadence, how much they run for you, and where the node bill lands.
Best Log Management Tools in 2026: What You Actually Pay For
Every log platform looks affordable at proof-of-concept volume and expensive at production volume. The pricing model, not the feature list, decides which one you can live with.
Your CI Runner Is the Target: Hardening Against npm Worms
The keyv compromise reached 444 packages and over two billion monthly installs through preinstall scripts. The controls that actually stop it are boring and mostly free.
You might have missed
Evergreen posts worth revisiting.