What Is a Dead Letter Queue (DLQ)?

A dead letter queue (DLQ) is a separate queue that holds messages a consumer could not process successfully. When a message has been retried up to a configured limit, or has been explicitly rejected, the broker stops redelivering it and moves it to the dead letter queue instead of discarding it or retrying forever.

The purpose is containment. Without a DLQ, a single message that always fails has two possible fates, and both are bad: it is redelivered endlessly, blocking the queue and burning consumer capacity, or it is dropped and the failure disappears silently. A dead letter queue gives that message a third destination — somewhere it can be inspected, diagnosed, and replayed once the underlying problem is fixed. The pattern is standard equipment in every mature message broker, though the names differ: dead letter queue, dead letter exchange, dead letter topic.

How a Dead Letter Queue Works

Dead-lettering is an extension of the ordinary acknowledgment loop rather than a separate mechanism. A typical delivery cycle runs like this:

  1. A consumer receives a message from the queue. The broker marks it invisible to other consumers for a visibility timeout and increments its delivery count.
  2. The consumer processes the message and sends an acknowledgment. The broker removes it and the cycle ends.
  3. If the consumer instead crashes, times out, or sends a negative acknowledgment (NACK), the message becomes visible again and is redelivered to another consumer.
  4. Each redelivery increments the delivery count. Once that count exceeds the configured delivery limit, the broker routes the message to the dead letter queue rather than making it available again.

Two settings do most of the work. The delivery limit (called maxReceiveCount in Amazon SQS and maxDeliveryCount in Azure Service Bus) decides how many attempts a message gets. The visibility timeout decides how long each attempt lasts before the broker assumes the consumer has failed. Set the timeout shorter than your real processing time and messages will be redelivered while they are still being handled, exhausting the delivery limit on work that was actually succeeding.

Most brokers dead-letter on more than delivery-count exhaustion alone. Message expiry, queue length limits, oversized headers, and errors evaluating a subscription filter can all send a message to the DLQ. Crucially, a dead letter queue is usually an ordinary queue: you read from it, write to it, and monitor it with the same tools you use everywhere else.

Why Messages Fail

Deciding how to configure dead-lettering means understanding what kind of failure you are protecting against. There are three, and they want different treatment.

Poison messages. A message that can never succeed, no matter how many times it is retried — a malformed payload, a schema change the consumer does not understand, a missing deserializer, a field the business logic rejects outright. Retrying is pure waste. These are what a DLQ exists for. In a strictly ordered queue, a single poison message can also block every message behind it, turning one bad payload into a full outage.

Transient failures. A network blip, a downstream service restarting, a database connection exhausted for a few seconds. The message is fine; the environment was briefly not. These should be retried, ideally with backoff, and should not reach the DLQ. If they do, your delivery limit is too low.

Semi-permanent failures. A downstream dependency that has been down for twenty minutes, or a reference record that has not been created yet. Retries will eventually succeed, but not within any reasonable delivery limit. These are the hardest case, and they are the reason a DLQ needs a replay path rather than just an alert.

Dead Letter Queue vs. Retry Queue vs. Dead Letter Topic

Three related terms get used interchangeably and should not be.

A dead letter queue is terminal storage. Messages arrive there after the automatic machinery has given up, and nothing moves them out again without human or scheduled intervention.

A retry queue is intermediate. It holds a failed message for a delay — often growing exponentially across successive attempts — and then feeds it back to the main queue automatically. Systems that need graduated backoff frequently chain several retry queues with increasing delays before a message finally lands in the DLQ. Some brokers make this unnecessary by supporting a per-message delay on the negative acknowledgment itself.

A dead letter topic (DLT) is the publish/subscribe equivalent. The distinction that matters is that a topic has many independent subscribers, and a message that crashes one may be processed perfectly by another. Dead-lettering therefore belongs to the subscription, not the topic: each subscriber gets its own delivery limit and its own dead letter destination. A real-time analytics consumer might give up after one attempt, while a financial audit log retries twenty times before dead-lettering. Configuring dead-lettering at the topic level would force every subscriber to share one failure policy.

Dead Letter Queues Across Message Brokers

Every major broker implements the pattern, with different names and different trigger conditions.

SystemNameKey setting
Amazon SQSDead-letter queuemaxReceiveCount in the redrive policy
RabbitMQDead letter exchange (DLX)x-dead-letter-exchange
Azure Service Bus$DeadLetterQueue sub-queueMaxDeliveryCount
Apache KafkaDead letter topicClient-side; no broker feature
Google Cloud Pub/SubDead letter topicmaxDeliveryAttempts
IBM MQSYSTEM.DEAD.LETTER.QUEUEQueue manager DEADQ attribute
JMS / Jakarta MessagingProvider-definedNot specified by the standard

Amazon SQS attaches a redrive policy to the source queue naming a target DLQ and a maxReceiveCount, which defaults to 10 and can be set anywhere from 1 to 1,000. The DLQ must be an ordinary SQS queue of the same type as its source — standard or FIFO — and AWS recommends keeping both in the same account and Region. SQS also provides a dead-letter queue redrive operation that moves messages back to the source queue or another queue of the same type, one of the few replay mechanisms any major broker ships in the box rather than leaving to the application.

RabbitMQ takes a different approach, routing through an exchange rather than directly to a queue. Setting x-dead-letter-exchange on a queue republishes dead-lettered messages to that exchange, optionally with a new routing key, which lets a single DLX fan failed messages out to different destinations by type. Messages are dead-lettered when they are rejected with requeue=false, when their TTL expires, or when the queue exceeds a length limit. Delivery-count-based dead-lettering, however, is a quorum queue feature only — classic queues do not support x-delivery-limit at all, which catches teams out regularly. Since RabbitMQ 4.0, quorum queues also apply a default delivery limit of 20, whereas earlier versions retried indefinitely. Messages exceeding it are dead-lettered if a DLX is configured and dropped if one is not, so upgrading without a dead letter exchange in place can silently discard messages that used to loop forever.

Apache Kafka has no broker-side dead letter mechanism, because consumers manage their own offsets and Kafka does not track per-message delivery attempts. Dead-lettering lives entirely in the client. Kafka Connect supports it through errors.tolerance and errors.deadletterqueue.topic.name; Spring Kafka provides DeadLetterPublishingRecoverer with a DefaultErrorHandler, publishing to a .DLT topic by default. Both are library conventions rather than platform guarantees.

Azure Service Bus gives every queue and subscription an automatic dead-letter sub-queue, reachable at a path like orders/$deadletterqueue, so there is nothing to provision. MaxDeliveryCount defaults to 10 and cannot be disabled — only raised. Two other triggers are opt-in and frequently misunderstood: dead-lettering on message expiry requires EnableDeadLetteringOnMessageExpiration, which is false by default, so expired messages otherwise vanish rather than being captured, and dead-lettering on subscription filter errors must likewise be enabled explicitly. Oversized headers are dead-lettered automatically. Every dead-lettered message carries a DeadLetterReason and DeadLetterErrorDescription, which makes Service Bus unusually good at answering the question of why a message failed.

The JMS specification is silent on the subject. It defines delivery and acknowledgment thoroughly but leaves failure routing to the provider, which is why dead-lettering behaviour varies so widely between ActiveMQ, IBM MQ, and WebLogic despite a shared API.

What to Do After a Message Reaches the Dead Letter Queue

Configuring a DLQ is the easy half. The half that gets skipped is deciding what happens next — and a dead letter queue nobody reads is just a slower way to lose data.

Alert on depth and age. Queue depth alone is a weak signal, because a DLQ that has held three messages for a month looks identical to one that received three messages a minute ago. Alert on the age of the oldest message as well, and on the arrival rate. A sudden burst usually means a deployment or a schema change, not three unrelated bad payloads.

Preserve the failure context. A payload on its own rarely explains anything. Capture the exception, the consumer that failed, the delivery count, and the original timestamp — most brokers support message headers or properties for exactly this. Without them, triage means reproducing the failure from scratch.

Build the replay path before you need it. Because a DLQ is an ordinary queue, replaying is mechanically simple: read messages, publish them back to the source, acknowledge them in the DLQ. What matters is having this ready as a tested, rate-limited operation rather than improvising it during an incident. Replay must also be safe to run twice, which in practice means consumers should be idempotent.

Bound its growth. Apply a TTL, a maximum size, or both. A DLQ on an in-memory store consumes the same RAM as production data, and an unbounded one that quietly accumulates for months is a memory incident waiting to happen.

Dead Letter Queue Anti-Patterns

  • Setting the delivery limit too low. A limit of 1 or 2 dead-letters messages during ordinary transient blips. Allow enough attempts for a brief outage to resolve itself.
  • Treating the DLQ as an archive. It is a work queue for failures, not cold storage. If messages accumulate without anyone processing them, the failure was never handled — only deferred.
  • Dead-lettering without alerting. A DLQ that grows silently converts a loud failure into a quiet one, which is strictly worse.
  • No replay tooling. Without a way back, every dead-lettered message becomes a manual database fix.
  • Unbounded growth. No TTL and no size cap turns a messaging problem into a capacity problem.
  • Configuring dead-lettering per topic instead of per subscription. In pub/sub this forces unrelated consumers with different tolerances to share one failure policy.
  • Replaying blindly. Pushing an entire DLQ back to the source queue without first fixing the cause simply refills it, and can amplify the original incident.

Dead Letter Queues on Redis and Valkey

Neither Redis nor Valkey provides dead-lettering natively, because neither provides the delivery accounting it depends on.

List-based queues built with LPUSH and BRPOP have no concept of acknowledgment at all — once a worker pops a message it is gone from the server, so a crash mid-processing loses it outright. The usual workaround is LMOVE into a processing list, but retry counting, timeout detection, and dead-lettering all remain application code.

Streams come considerably closer. Consumer groups track unacknowledged messages in a pending entries list, and XPENDING exposes a per-message delivery count that XCLAIM and XAUTOCLAIM increment as stalled messages are reclaimed. The raw material for a DLQ is genuinely there. What is missing is the automation: nothing acts on that delivery count for you. You write the loop that inspects it, decides the limit is exhausted, writes the message to a second stream, and acknowledges it on the first — ideally as a Lua script, since doing it in two round trips risks losing the message in between. That is a meaningful amount of infrastructure code to write, test, and operate.

Dead Letter Queues With Redisson PRO

Redisson is a Java client for Redis and Valkey. Its PRO edition supplies the delivery accounting the native primitives lack, so dead-lettering becomes configuration rather than application code.

Reliable Queue (RReliableQueue) handles the point-to-point case. A delivery limit, a visibility timeout, and a dead letter destination are set once on the queue:

RReliableQueue<Order> queue = redisson.getReliableQueue("orders");

queue.setConfigIfAbsent(QueueConfig.defaults()
    .deliveryLimit(5)
    .visibilityTimeout(Duration.ofSeconds(60))
    .timeToLive(Duration.ofHours(24))
    .deadLetterQueueName("orders-dlq"));

A message that fails five times — whether through an explicit negative acknowledgment or a consumer that never acknowledged at all — moves to orders-dlq automatically. One detail is worth knowing: deliveryLimit defaults to 10 and applies whether or not a dead letter queue exists. Without deadLetterQueueName set, a message that exhausts its attempts is simply deleted, so configuring the DLQ is what turns the limit from a discard rule into a safety net. Consumers can also reject a message immediately with negativeAcknowledge, optionally with a delay before the next attempt, which covers graduated retry without a chain of intermediate queues.

Because the DLQ is itself an RReliableQueue, inspecting and replaying it uses the same API as anything else — no special tooling and no separate client:

RReliableQueue<Order> dlq = redisson.getReliableQueue("orders-dlq");

List<Message<Order>> failed = dlq.pollMany(QueuePollArgs.defaults()
    .acknowledgeMode(AcknowledgeMode.MANUAL)
    .count(100));

for (Message<Order> msg : failed) {
    queue.add(QueueAddArgs.messages(MessageArgs.payload(msg.getPayload())));
    dlq.acknowledge(QueueAckArgs.ids(msg.getId()));
}

Reliable PubSub (RReliablePubSubTopic) covers the publish/subscribe side, and configures dead letter topics per subscription for the reason described earlier — each subscriber owns its own failure tolerance:

RReliablePubSubTopic<OrderEvent> topic = redisson.getReliablePubSubTopic("events");

Subscription<OrderEvent> subscription = topic.createSubscription(
    SubscriptionConfig.name("auditLog")
        .deliveryLimit(20)
        .deadLetterTopicName("events-dlt"));

Both objects also support message priority, delayed delivery, deduplication, time-to-live, queue and message size limits, and message headers — the last being useful for carrying failure context into the DLQ. The same behaviour is available through the JMS API and configurable through JNDI property files, covered in dead letter queues and dead letter topics with Valkey/Redis based JMS.

Reference material is in the Reliable Queue documentation and the Reliable PubSub documentation. Both are Redisson PRO features and can be evaluated with a free trial.

Dead Letter Queue: Frequently Asked Questions

What Does DLQ Stand For?

DLQ stands for dead letter queue. It is a separate queue that holds messages a consumer could not process successfully, either because the configured delivery limit was exhausted or because the message was explicitly rejected. The term borrows from postal services, where undeliverable mail is sent to a dead letter office.

What Is a Dead Letter Queue Used For?

A dead letter queue isolates messages that repeatedly fail so they stop consuming retry capacity and stop blocking the messages behind them, while still being preserved for inspection. It prevents both infinite retry loops and silent data loss, and gives teams a single place to diagnose failures and replay the affected messages once the cause is fixed.

What Is the Difference Between a Dead Letter Queue and a Retry Queue?

A retry queue holds a failed message for a delay and then returns it to the main queue automatically, so processing is attempted again. A dead letter queue is terminal: messages arrive after retries are exhausted and stay there until something deliberately moves them. Retry queues handle transient failures; dead letter queues handle failures that retrying will not fix.

Does Kafka Have a Dead Letter Queue?

Not at the broker level. Kafka consumers manage their own offsets and Kafka does not track per-message delivery attempts, so there is nothing for the broker to act on. Dead-lettering is implemented client-side instead — Kafka Connect provides it through the errors.deadletterqueue.topic.name setting, and Spring Kafka through DeadLetterPublishingRecoverer, which publishes failed records to a separate topic.

Does Redis Have a Dead Letter Queue?

Not natively. Redis lists have no acknowledgment mechanism, and while Redis Streams track a per-message delivery count in the pending entries list, nothing acts on that count automatically — moving an exhausted message to a second stream is application code you write and maintain. A client such as Redisson PRO adds delivery limits and dead letter queues as configuration on top of Redis or Valkey. The same applies to Valkey, which inherits the same primitives and the same limitations.

How Do You Reprocess Messages From a Dead Letter Queue?

Fix the underlying cause first, then read the messages from the DLQ and republish them to the source queue. Because a dead letter queue is usually an ordinary queue, this needs no special mechanism — Amazon SQS provides a built-in redrive operation, and other systems use a small replay job. Rate-limit the replay so it does not overwhelm consumers, and make sure the consumer is idempotent, since replayed messages may have been partially processed before they failed.

Similar terms