Sitemap

The Dead Letter Queue Is Not Optional

8 min readJul 23, 2026

--

If a message fails and no one is alerted, did it ever exist?

Press enter or click to view image in full size
Photo by Alessia Bosi on Unsplash

The Message That Vanished

Here is a thing that happened in a production system I once audited. A payment service consumed order events from a Pulsar topic. One day, a third-party API changed its response format. The consumer tried to deserialize the new payload, got a `JsonMappingException`, and crashed. Pulsar redelivered the message. The consumer crashed again. Five redeliveries later, Pulsar gave up. The message expired. No log. No alert. No trace.

The order was for seven hundred euros.

Nobody noticed for three days. The customer called support. Support investigated. The order was not in the database. It was not in any queue. It had been consumed five times and discarded. It had simply ceased to exist.

This is not a story about a bug. A breaking API change is a routine operational hazard. This is a story about an architectural decision: the team had not configured a Dead Letter Queue. They had retries, which they assumed was enough. And when retries exhausted themselves against a deterministic failure, the message vanished without a sound.

A Dead Letter Queue would have caught it. The message would have landed in `<topic>-dlq`. An alert would have fired. Someone would have inspected the payload, seen the new JSON shape, fixed the consumer, replayed the message, and moved on. Instead, a seven-hundred-euro order evaporated.

The cost of not configuring a DLQ was not the message itself. It was three days of invisibility, a customer escalation, and a support engineer digging through logs to reconstruct an event that the system had erased.

Retries Are Not Enough

The most common objection to configuring a Dead Letter Queue is: “We have retries. That’s sufficient.”

It is not.

Retries solve transient failures. A network blip. A brief database connection timeout. A downstream service restart that completes faster than the retry interval. These are the problems retries were built for, and they work beautifully for them. Exponential backoff, jitter, max attempts. The whole machinery of retry logic is designed around the assumption that the failure is temporary.

But many failures are not temporary. A malformed payload will be malformed on the hundredth attempt exactly as it was on the first. A consumer that crashes on a null pointer will crash again on redelivery, because the message has not changed and the code has not changed. A serialization error triggered by a breaking API response format does not heal itself. The retry loop becomes a death spiral: the same message, the same failure, the same redelivery, the same failure, N times, until the broker gives up and deletes it.

The retry mechanism is a safety net for the infrastructure. It is not a safety net for your application logic. Confusing the two is how messages disappear.

A DLQ is the safety net for your application logic. When the retries are exhausted, the message does not vanish. It moves sideways — off the main processing path, into a quarantine topic where it can be inspected, diagnosed, and, once the root cause is fixed, replayed. Five lines of configuration in Pulsar. A well-understood manual pattern in Kafka. And yet, in my experience, deployed in fewer than half the production messaging systems I encounter.

The Three Lies We Tell Ourselves

Teams that operate without a DLQ are not being reckless. They are usually operating under one of three assumptions, each of which is plausible and wrong.

Lie One: “It won’t happen to us.”

It will. A breaking upstream API change. A corrupted message from a buggy producer. A schema evolution that the consumer’s deserializer does not handle. A database migration that changes a column type. These are not exotic edge cases. They are Tuesday.

The question is not whether a poison message will enter your system. The question is whether you will know about it when it does. Without a DLQ, the answer is no.

Lie Two: “We’ll notice the data is missing.”

You probably will not. At least, not quickly. A missing order in a high-throughput system looks identical to no order at all. A metric showing “messages consumed” will still show consumption. The consumer did read the message, it just failed to process it. The retries are hidden inside the consumer lifecycle. By the time the message expires, the only evidence that it ever existed is gone.

This is what makes silent message loss so dangerous. It generates no crash. No error spike. No dashboard anomaly. It subtracts from your system one message at a time, and the subtraction is invisible.

Lie Three: “Configuring a DLQ is too complex.”

In Pulsar, it is five lines:


consumer.newConsumer()
.deadLetterPolicy(DeadLetterPolicy.builder()
.maxRedeliverCount(5)
.deadLetterTopic(“persistent://tenant/ns/orders-dlq”)
.build())
.subscribe();

In Amazon SQS, it is a dropdown in the console:

Dead-letter queue → Enabled → Maximum receives: 5.

In RabbitMQ, it is a policy declaration: `x-dead-letter-exchange` plus a TTL or a `x-max-length` limit.

Kafka is the outlier, no native DLQ, so you build your own. A `ConsumerInterceptor` that catches deserialization failures and routes them to `<topic>.dlq`. A wrapper around your `poll()` loop that checks for retry exhaustion and produces the failed message to a quarantine topic. Kafka’s design philosophy is to push this responsibility to the application layer, which is a defensible choice, but it means you have to write the code. And most teams do not, because it genuinely is more work.

Get Pierre-Sylvain AUGEREAU’s stories in your inbox

Join Medium for free to get updates from this writer.

But here is the thing: even a minimal, imperfect Kafka DLQ, one that catches deserialization errors and logs them, is better than nothing. You do not need the full alerting-plus-replay pipeline on day one. You just need the message to survive.

DLQ Across the Messaging Landscape

Apache Pulsar

  • Native DLQ: Yes
  • `DeadLetterPolicy` in consumer builder. Retries + DLQ in one configuration block.
  • The retry topic and DLQ are auto-created; you may want explicit naming control.

Apache Kafka

  • Native DLQ: No
  • Manual: catch exception, extract original `ConsumerRecord`, produce to `<topic>.dlq` with diagnostic headers.
  • Forgetting to include the original headers (partition, offset, timestamp) makes replay impossible.

RabbitMQ

  • Native DLQ: Partial
  • `x-dead-letter-exchange` + `x-message-ttl` or `x-delivery-limit`. Declarative, routing-key-based.
  • DLX messages lose their original routing key unless you store it as a header.

Amazon SQS

  • Native DLQ: Yes
  • Redrive policy: `maxReceiveCount`, target DLQ ARN. Fully managed.
  • Messages in DLQ do not trigger CloudWatch alarms by default; you have to set up your own.

The pattern is the same across all four. A message fails. It is retried up to a threshold. It is moved to a DLQ. The DLQ is monitored. A human or an automated process inspects it. The root cause is fixed. The message is replayed.

What varies is how much of this the platform does for you, and how much you have to build yourself. But in every case, the alternative to building it is not “simplicity.” It is invisible data loss.

The Real Pattern: DLQ + Alert + Replay

A Dead Letter Queue that nobody monitors is a cemetery. It preserves the bodies, but nobody comes to identify them. The value of a DLQ is not the queue itself. It is the pipeline around it.

Step one: the DLQ. The message lands in the dead letter topic with its original payload intact, plus diagnostic metadata: the exception message, the stack trace, the number of retry attempts, the original topic, partition, and offset. These headers make the difference between “a message failed” and “a message failed *because the payment gateway changed its error code from `DECLINED` to `PAYMENT_DECLINED`*.”

Step two: alerting. Every message that enters the DLQ should generate an alert. Not necessarily a PagerDuty alarm or a Slack notification is often enough for the first message. But the alert must fire. A DLQ with no alerting is functionally equivalent to no DLQ at all, because nobody will look until it is too late.

This is where the economics get interesting. A DLQ alert is a leading indicator. It tells you something is wrong before the customer calls. Compare that to the cost of debugging a silent data loss incident: searching logs for messages that should exist but do not, correlating producer and consumer offsets, reconstructing state from database backups. The DLQ alert costs you a notification. The silent loss costs you hours of engineering time, a customer escalation, and reputational damage.

Step three: inspection and replay. The DLQ should be queryable. A dashboard. A CLI tool. A minimal web UI. Someone on call should be able to see the failed message, read the error, and decide: can this be replayed as-is, or does it need transformation? Once the consumer is fixed, the messages are replayed back to the original topic, and the system heals itself.

This is not a luxury. It is the difference between a queue that stores failures and a queue that enables recovery.

The Silent Error Doctrine

Software engineers are trained to fear loud failures. Crashes. Outages. Incidents that page the on-call engineer at 3 AM. We build redundancy, load balancers, health checks, circuit breakers ; an entire discipline of resilience engineering to prevent these.

But the most expensive failures I have encountered in production systems were not loud. They were silent.

A payment message lost because the consumer failed to parse a field that nobody realized had changed. A configuration event that expired in a retry loop while a cluster drifted out of spec. A sensor reading that should have triggered a maintenance alert but did not, because the reading was discarded before anyone saw it.

None of these triggered an alarm. None of them crashed the system. Each of them eroded the system’s correctness one event at a time, invisible and cumulative.

This is the paradox at the heart of resilient messaging: the defenses you build against visible failure, retries, backoff, graceful degradation are the very things that make invisible failure possible. The retries mask the problem. The backoff delays detection. The graceful degradation turns data loss into a non-event from the system’s perspective.

The Dead Letter Queue is the mechanism that pierces this paradox. It says: if a message cannot be processed after a reasonable number of attempts, do not discard it. Quarantine it. Surface it. Make it visible.

A system without a DLQ is a system that has chosen to handle its most dangerous failures (the silent ones) by erasing the evidence.

One Line That Saves You

The reason most teams skip the DLQ is not technical. It is psychological. The DLQ is for failures you have not yet imagined. Configuring it feels like preparing for a disaster that may never happen, and humans are bad at investing in invisible risks.

But the question is not whether a poison message will enter your system. It is whether you will know about it when it does.

If you are running Pulsar, add `deadLetterPolicy` to your consumer configuration. It is five lines. If you are running Kafka, write the `ConsumerInterceptor` that catches `DeserializationException` and routes to a DLQ. It is one class. If you are running SQS, enable the redrive policy. It is one console click.

Do it now. Because the poison message is already in transit, or it will be on Tuesday. And the only question that matters is whether your system will tell you, or whether it will swallow the evidence and keep running, perfectly healthy, perfectly silent, one message lighter than it was before.

Sources

--

--