Sitemap
TheVega.AI

TheVega.AI

DevOps Engineer passionate about teaching and sharing knowledge. Writing about DevOps, SRE, Linux, Kubernetes, and computer science behind modern infrastructure

Press enter or click to view image in full size
When messages fail repeatedly, a DLQ keeps the failure contained.

When Messages Keep Failing: Designing Reliable AWS Systems with Dead-Letter Queues

13 min readAug 30, 2026

--

Distributed systems are designed with one uncomfortable assumption:

Things will fail.

A database can become unavailable.

An API can time out.

A Lambda function can throw an exception.

A message can contain invalid data.

A downstream service can return an unexpected response.

And when your application uses asynchronous messaging, there’s another problem:

What happens to a message that keeps failing?

Imagine this:

Producer
|
v
SQS Queue
|
v
Lambda
|
X
Processing Failed
|
v
Retry
|
X
Processing Failed
|
v
Retry
|
X
Processing Failed

What happens next?

You don’t want the same broken message consuming resources forever.

This is where a Dead-Letter Queue (DLQ) becomes extremely useful.

A DLQ gives repeatedly failing messages somewhere to go after they exceed an appropriate retry threshold.

Instead of allowing one problematic message to repeatedly interfere with normal processing, you isolate it for investigation and recovery.

In this article, we’ll understand how DLQs work, why they’re important, how they fit into SQS and Lambda architectures, and how to design them properly for production systems.

What Is a Dead-Letter Queue?

A Dead-Letter Queue is a queue used to hold messages that could not be successfully processed after a configured number of attempts.

The basic flow looks like this:

Main Queue
|
v
Consumer
|
X Failure
|
v
Retry
|
X Failure
|
v
Retry
|
X Failure
|
v
DLQ

The message isn’t simply discarded.

Instead, it is moved aside so that engineers or automated recovery processes can investigate it.

Think of a DLQ as:

A quarantine area for messages that need special attention.

Why Do We Need a DLQ?

Consider an order-processing system:

Order Service
|
v
SQS Queue
|
v
Order Lambda

Most messages work:

Order 1 → Success
Order 2 → Success
Order 3 → Success
Order 4 → Success

But then:

Order 5 → Failure

Maybe the message is malformed.

The consumer retries it.

Order 5 → Retry → Failure

Again:

Order 5 → Retry → Failure

And again.

Without a DLQ, the system can waste processing capacity repeatedly attempting the same problematic message.

With a DLQ:

Order 5
|
v
Retry
|
v
Retry limit reached
|
v
DLQ

The main queue can continue processing other messages.

A DLQ Doesn’t Mean the Message Is “Dead”

The name can be misleading.

A dead-letter message isn’t necessarily permanently unusable.

It means:

The normal processing path couldn’t successfully handle it within the configured retry policy.

The message may still be recoverable.

For example:

DLQ
|
v
Investigate
|
+----> Fix application bug
|
+----> Fix message data
|
+----> Restore dependency
|
v
Replay message

The DLQ creates an opportunity to recover rather than silently losing the message.

The Basic SQS + DLQ Architecture

With Amazon SQS, a common architecture looks like:

Producer
|
v
+-------------+
| Main Queue |
+-------------+
|
v
Consumer
|
+---- Success → Delete
|
+---- Failure → Retry
|
v
DLQ

The key configuration is the redrive policy.

It determines when a message should be moved from the source queue to the dead-letter queue based on its receive count.

For example:

maxReceiveCount = 5

Conceptually:

Attempt 1 → Failed
Attempt 2 → Failed
Attempt 3 → Failed
Attempt 4 → Failed
Attempt 5 → Failed
|
v
DLQ

The exact behavior and configuration should be designed according to your workload and AWS service integration.

The Most Important Question: Why Did the Message Fail?

Not all failures are the same.

Consider two scenarios.

Scenario 1: Temporary failure

Lambda
|
v
Payment API
|
X
Timeout

The payment provider might recover a few seconds later.

A retry makes sense.

Scenario 2: Permanent failure

Lambda
|
v
Message
|
X
Invalid JSON / Missing required field

Retrying the exact same message probably won’t fix anything.

This distinction is critical.

A good retry strategy handles transient failures while preventing permanent failures from endlessly consuming resources.

Transient vs Permanent Failures

Let’s classify some examples.

Transient failures

These may succeed later:

Temporary network error
API timeout
Database connection issue
Temporary service outage
Throttling

Retries can be useful.

Permanent failures

These usually require a correction:

Invalid message format
Missing required field
Unsupported event type
Invalid business state
Corrupted data

Retries alone won’t solve these.

That’s why DLQs are useful.

They separate:

"Try again"

from:

"Something needs investigation."

A Real Example: Payment Processing

Imagine an e-commerce system:

Order Service
|
v
Payment Queue
|
v
Payment Lambda
|
v
Payment Provider

The queue contains:

{
"eventId": "evt-1001",
"orderId": "ORD-12345",
"amount": 2499
}

The Payment Lambda calls the payment provider.

Suppose the provider is temporarily unavailable.

Payment Lambda
|
v
Payment API
|
X
Timeout

The message can be retried.

If the provider recovers:

Retry
|
v
Payment API
|
v
Success

Great.

But imagine the message itself is invalid:

{
"orderId": "ORD-12345"
}

and the payment amount is missing.

Retrying this message five times won’t magically create the missing amount.

Eventually:

Invalid Message
|
v
Retries
|
v
DLQ

Now engineers can investigate the event.

Poison Messages

A message that repeatedly fails processing is often called a poison message.

For example:

Message A → Success
Message B → Success
Message C → Failure
Message C → Failure
Message C → Failure
Message C → Failure

Message C is effectively poisoning the normal processing path.

A DLQ isolates it:

Main Queue
----------------
A
B
D
E
F

C
|
v
DLQ

The healthy workload can continue.

What Happens Without a DLQ?

Suppose a consumer keeps receiving the same bad message.

You could end up with:

Bad Message
|
v
Attempt 1
|
v
Attempt 2
|
v
Attempt 3
|
v
Attempt 4
|
v
Attempt 5
|
...

This creates several problems:

  • Wasted compute
  • Increased logs
  • Increased downstream traffic
  • Higher Lambda invocation count
  • Longer processing delays
  • More difficult troubleshooting

A DLQ gives the system a controlled escape path.

DLQs and Lambda

A very common architecture is:

SQS
|
v
Lambda
|
X
Failure
|
v
SQS DLQ

Lambda consumes messages from the SQS queue.

When processing fails according to the configured behavior, the message can become available again.

After enough receive attempts, SQS can move it to the DLQ according to the queue’s redrive policy.

Conceptually:

              Main Queue
|
v
Lambda
|
+----+----+
| |
Success Failure
| |
v v
Delete Retry
|
v
DLQ

This is one of the most common serverless failure-handling patterns on AWS.

Don’t Confuse Retry With Recovery

Retries answer:

“Can this work if I try again?”

A DLQ answers:

“What should happen when normal retries aren’t enough?”

These are different mechanisms.

A production workflow often looks like:

Message
|
v
Process
|
+---- Success
|
+---- Temporary failure
|
v
Retry
|
+---- Success
|
+---- Failure
|
v
DLQ

This creates a layered failure strategy.

Choosing maxReceiveCount

One of the most important DLQ configuration decisions is:

How many times should a message be retried before going to the DLQ?

There isn’t one universal answer.

It depends on the workload.

Suppose an external API occasionally fails for a few seconds.

You may want several retries.

But if messages are usually failing because of invalid data, excessive retries provide little value.

Think about:

Failure frequency
Failure type
Recovery time
Processing cost
Business importance
Downstream rate limits

Don’t blindly choose:

maxReceiveCount = 100

just because more retries sound safer.

More Retries Aren’t Always Better

Imagine:

10,000 messages

and:

1,000 messages

are permanently invalid.

If each invalid message is retried 20 times:

1,000 × 20 = 20,000 unnecessary processing attempts

You have created additional load without increasing the chance of success.

A better retry strategy distinguishes transient problems from permanent ones.

Visibility Timeout and DLQ

DLQ behavior is closely connected to the SQS visibility timeout.

Remember:

Receive message
|
v
Message becomes temporarily invisible
|
v
Consumer processes it

If processing fails and the message isn’t successfully deleted, it can become visible again after the visibility timeout.

Then:

Attempt 1
Attempt 2
Attempt 3
...

The receive count increases.

Eventually, the message can be moved to the DLQ according to the redrive configuration.

So these settings need to work together:

Visibility Timeout
+
Retry behavior
+
maxReceiveCount
+
DLQ

Don’t configure them independently without understanding the entire message lifecycle.

Visibility Timeout Should Match Processing

Suppose your Lambda normally takes:

30 seconds

to process a message.

But your visibility timeout is:

10 seconds

You could end up with:

Lambda A
|
| Processing
|
+---- 10 sec
|
v
Message visible again
|
v
Lambda B receives it

Now two consumers may process the same message.

This can result in duplicate side effects.

The visibility timeout should therefore be designed around the actual processing characteristics of the workload.

Idempotency Is Still Required

This is perhaps the most important point about DLQs:

A DLQ does not eliminate duplicate processing.

Imagine:

Message
|
v
Lambda
|
v
Payment succeeds
|
X
Delete doesn't complete

The message can be processed again.

You could get:

Payment attempt 1 → Success
Payment attempt 2 → Success

That’s why your consumer needs appropriate idempotency controls.

For example:

{
"eventId": "evt-12345"
}

The application can use the event ID to determine whether the operation has already been successfully processed.

DLQ and Idempotency Work Together

Think of them as solving different problems.

Idempotency

Protects against:

Same message processed multiple times

DLQ

Protects against:

Message repeatedly failing normal processing

Together:

                 Message
|
v
Consumer
/ \
/ \
Duplicate? Failure?
| |
Yes Yes
| |
v Retry
Ignore |
v
DLQ

Both are important in reliable asynchronous systems.

What Should You Put in a DLQ?

Ideally, the DLQ contains the original message plus enough context to investigate the failure.

Get TheVega.AI’s stories in your inbox

Join Medium for free to get updates from this writer.

For example:

{
"eventId": "evt-12345",
"eventType": "OrderCreated",
"orderId": "ORD-10001",
"timestamp": "2026-08-30T10:30:00Z"
}

You may also want your application logs to contain:

eventId
requestId
orderId
consumer
error type
error message
timestamp

This makes correlation much easier.

Press enter or click to view image in full size
From failed messages to safe recovery: detect, fix, and replay.

Don’t Put Sensitive Data in Logs

While debugging DLQ messages, it’s tempting to log everything.

Be careful.

If a message contains:

Customer data
Payment information
Authentication tokens
Personal information

don’t blindly copy the entire message into logs.

Use structured identifiers and carefully selected diagnostic fields.

For example:

eventId=evt-12345
orderId=ORD-10001
error=InvalidPaymentAmount

is often much safer than dumping the entire payload.

Monitoring the DLQ

Creating a DLQ isn’t enough.

You need to know when messages enter it.

Imagine your DLQ contains:

0 messages
0 messages
0 messages
1 message
250 messages
1,500 messages

Something has changed.

Potential causes:

Application deployment
Schema change
Downstream outage
Permission change
Invalid producer behavior
Dependency failure

A DLQ should therefore be treated as an operational signal.

What Should You Alert On?

A simple alert might trigger when:

ApproximateNumberOfMessagesVisible > 0

But the exact threshold depends on your workload.

For a critical payment workflow:

1 failed message

might deserve attention.

For a high-volume analytics pipeline:

A small number of failed messages

might not require immediate intervention.

The right alert threshold depends on business impact.

Don’t Alert on Every Retry

This is another important operational principle.

Suppose a transient database problem causes:

Message → Retry → Success

You probably don’t want an engineer paged every time a retry occurs.

Instead, alert on meaningful conditions such as:

DLQ receives messages
Queue backlog grows unexpectedly
Oldest message age crosses threshold
Consumer errors increase significantly

This helps avoid alert fatigue.

DLQ as an Observability Signal

A DLQ can tell you something about the health of your application.

For example:

Application deployment
|
v
DLQ messages increase
|
v
Something in the new release
may be incompatible

Or:

Schema change
|
v
Consumer rejects messages
|
v
DLQ grows rapidly

The DLQ isn’t just a recovery mechanism.

It’s also a valuable failure signal.

A Production Monitoring View

For an SQS + Lambda architecture, think about:

                    SQS
|
+----------+----------+
| |
Main Queue DLQ
| |
v v
Lambda Investigation
|
v
Downstream API

Monitor:

Main queue

  • Queue depth
  • Oldest message age
  • Message throughput

Lambda

  • Errors
  • Duration
  • Throttles
  • Concurrency

DLQ

  • Message count
  • Arrival rate
  • Oldest message age

Downstream dependency

  • Error rate
  • Latency
  • Availability

This gives you visibility across the entire processing path.

DLQ Does Not Fix the Root Cause

This is worth emphasizing.

Suppose:

DLQ = 10,000 messages

You shouldn’t consider the problem solved because the messages were successfully moved there.

The DLQ is a containment mechanism, not a root-cause fix.

You still need to determine:

Why did these messages fail?

Possible root causes:

Bug
Bad data
Schema incompatibility
Permission issue
Database outage
External API failure
Configuration error

The DLQ gives you breathing room to investigate.

The Recovery Workflow

A good DLQ process might look like:

              DLQ
|
v
Detect Message
|
v
Investigate
|
+------+------+
| |
Permanent Temporary
problem problem
| |
v v
Correct data Restore service
| |
+------+------+
|
v
Replay
|
v
Main Queue
|
v
Process

The exact replay process depends on the application.

But the important idea is:

DLQ handling should be an operational workflow, not an afterthought.

Replay Is Powerful — And Dangerous

Suppose you’ve fixed the bug.

You now have:

DLQ
|
+-- 10,000 messages

You want to replay them.

Before doing that, ask:

  • Is the consumer fixed?
  • Are the messages still valid?
  • Will replay create duplicate side effects?
  • Can the downstream service handle the traffic?
  • Should messages be replayed all at once?
  • Should they be replayed gradually?

Don’t blindly dump thousands of messages back into production.

Replay Gradually When Appropriate

Imagine:

DLQ
|
v
10,000 messages

and you immediately replay everything.

You might create:

10,000 messages
|
v
Main Queue
|
v
Lambda scales
|
v
Database
|
X
Overloaded

Now you’ve created another incident.

A safer approach may be:

DLQ
|
v
Replay small batch
|
v
Observe
|
v
Replay more
|
v
Observe

Recovery should respect downstream capacity.

The SNS + SQS + Lambda + DLQ Pattern

Let’s combine everything we’ve discussed.

                      Event Producer
|
v
SNS Topic
|
v
SQS Queue
|
v
Lambda
/ \
/ \
Success Failure
| |
v v
Delete Retry
|
v
Retry Limit
|
v
DLQ

For multiple consumers:

                         SNS
|
+------------+------------+
| |
v v
Payment SQS Analytics SQS
| |
v v
Payment Lambda Analytics Lambda
| |
v v
Payment DLQ Analytics DLQ

This architecture gives each consumer an independent failure boundary.

Why Separate DLQs Matter

Suppose you have:

SNS
|
+----> Payment Queue → Payment Lambda → Payment DLQ
|
+----> Analytics Queue → Analytics Lambda → Analytics DLQ

Now imagine Analytics is broken.

Its DLQ may fill up.

Payment can continue independently.

That’s a major benefit of giving each consumer its own queue and DLQ.

You avoid creating one giant failure domain.

DLQs and Event-Driven Architecture

As event-driven systems grow, DLQs become increasingly important.

Consider:

               Event Bus / SNS
|
+---------------+---------------+
| | |
v v v
Service A Service B Service C
| | |
SQS SQS SQS
| | |
Lambda Lambda Lambda
| | |
DLQ DLQ DLQ

Each service owns its processing lifecycle.

This means:

  • Service A can retry independently
  • Service B can fail independently
  • Service C can recover independently

That’s a powerful property in distributed systems.

Common DLQ Mistakes

Mistake 1: Creating a DLQ and Never Monitoring It

A DLQ without monitoring is just a hidden failure bucket.

Fix: Create meaningful alerts and dashboards.

Mistake 2: Setting the Retry Count Arbitrarily

More retries don’t necessarily mean more reliability.

Fix: Choose retry thresholds based on failure behavior.

Mistake 3: Ignoring Idempotency

A message can still be processed multiple times before reaching the DLQ.

Fix: Design consumers for safe retries.

Mistake 4: Replaying Everything at Once

This can overload downstream systems.

Fix: Replay carefully and monitor the system.

Mistake 5: Treating Every Failure as Permanent

Transient outages may recover after a retry.

Fix: Distinguish transient and permanent failures.

Mistake 6: Treating the DLQ as the Solution

Moving a message to a DLQ doesn’t fix why it failed.

Fix: Investigate the root cause.

Mistake 7: Keeping Messages in the DLQ Forever

Depending on retention and operational requirements, DLQ messages can eventually expire.

Fix: Define a recovery and retention strategy.

Choosing the Right DLQ Strategy

Before implementing a DLQ, ask:

What is a failed message?

Define what constitutes a processing failure.

How many retries make sense?

Base this on the workload.

How long can a message remain in the system?

Consider queue retention and business requirements.

How will failures be detected?

Use metrics and alerts.

Who owns the DLQ?

Someone should be responsible for investigating it.

How will messages be recovered?

Define a replay or remediation process.

Can replay create duplicate side effects?

Make sure consumers are idempotent.

Can the downstream system handle replay traffic?

Avoid turning recovery into another outage.

These questions turn a basic DLQ configuration into a production-ready failure-handling strategy.

A Practical Failure-Handling Architecture

A mature asynchronous system might look like:

                      Producer
|
v
SQS Queue
|
v
Lambda
|
+-----------+-----------+
| |
Success Failure
| |
v v
Delete Retry
|
v
Retry Threshold
|
v
DLQ
|
v
Alert / Review
|
v
Remediation
|
v
Replay

Notice something important.

The DLQ is part of a larger workflow.

It’s not just:

Queue → DLQ

It’s:

Failure → Containment → Detection → Investigation → Recovery

That’s the real operational model.

The Bigger Lesson

Distributed systems aren’t reliable because they never fail.

They’re reliable because they handle failure predictably.

A queue can absorb workload.

Retries can handle transient problems.

A DLQ can isolate messages that repeatedly fail.

Idempotency can protect against duplicate processing.

Monitoring can tell you when something is going wrong.

Together:

                    Message
|
v
Queue
|
v
Consumer
/ \
/ \
Success Failure
| |
v v
Delete Retry
|
v
Retry Limit
|
v
DLQ
|
v
Investigation
|
v
Recovery

That’s what resilient asynchronous architecture looks like.

Final Takeaway

A Dead-Letter Queue is more than a place where failed messages go.

It’s a mechanism for containing failure without allowing one problematic message to continuously disrupt the normal processing path.

The pattern is straightforward:

Main Queue
|
v
Consumer
|
+---- Success → Complete
|
+---- Failure → Retry
|
v
Retry Limit
|
v
DLQ

But production reliability comes from everything around it:

  • Choose retry limits carefully
  • Configure visibility timeout appropriately
  • Design consumers for idempotency
  • Monitor queue backlog and message age
  • Alert on meaningful DLQ activity
  • Investigate the root cause
  • Build a safe replay strategy
  • Protect downstream systems during recovery

The most important mindset is:

A DLQ isn’t where failed messages go to disappear. It’s where failures go to become visible, diagnosable, and recoverable.

If you design it that way, a DLQ becomes an important part of your reliability strategy rather than simply another AWS resource in your architecture.

How Do You Handle Failed Messages?

Have you implemented DLQs with SQS + Lambda, SNS → SQS fan-out, or another event-driven architecture?

What has caused the most DLQ messages in your systems — bad payloads, downstream outages, deployment bugs, schema changes, or configuration issues?

Share your experience in the comments.

If this article helped you understand how DLQs fit into reliable AWS architectures, share it with another AWS or DevOps engineer building asynchronous systems.

--

--

TheVega.AI

Written by TheVega.AI

DevOps Engineer passionate about teaching and sharing knowledge. Writing about DevOps, SRE, Linux, Kubernetes, and computer science behind modern infrastructure