AWS SQS Dead-Letter Queues

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Isolate and recover failing messages. Redrive policies, choosing maxReceiveCount, and why a DLQ without an alarm is worse than none at all.

A message that cannot be processed will be retried forever. It consumes consumer capacity, fills your logs with the same stack trace, and on a FIFO queue it blocks every other message in its group indefinitely. That single message is a poison pill.

A dead-letter queue is where SQS puts it after a set number of failed attempts, so healthy traffic keeps flowing and the failure becomes something you can inspect at leisure.

How a DLQ is wired up

A DLQ is an ordinary queue. What makes it a DLQ is the redrive policy on the source queue pointing at it.

aws sqs create-queue --queue-name orders-dlq \
  --attributes MessageRetentionPeriod=1209600
DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "$DLQ_URL" \
  --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

aws sqs set-queue-attributes --queue-url "$SRC_URL" --attributes "{
  \"RedrivePolicy\": \"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"
}"

Two constraints worth knowing before you build: the DLQ must be in the same region and account as the source queue, and it must be the same type. A FIFO source needs a FIFO DLQ.

Note the retention period. 1209600 seconds is 14 days, the maximum, and it is the right choice for a DLQ. The default is 4 days, which is easily a long weekend plus a Monday of higher-priority work, after which the evidence is gone.

What actually triggers a move

SQS counts receives, not failures. Every time a consumer receives a message and does not delete it, because it threw, crashed, timed out, or was killed mid-work, the receive count increases. Once it exceeds maxReceiveCount, the next attempt sends the message to the DLQ instead of making it visible again.

Two consequences follow from that, and both surprise people.

A visibility timeout that is too short burns through the count. If your handler needs 45 seconds and the timeout is 30, each message is received repeatedly while still being processed successfully, so perfectly good messages reach the DLQ. When you find healthy-looking messages in a DLQ, check the visibility timeout before you check the code.

Nothing has to raise an error. A consumer that silently drops a message without deleting it produces exactly the same counter behaviour as one that crashes.

aws sqs receive-message --queue-url "$SRC_URL" \
  --attribute-names ApproximateReceiveCount \
  --message-attribute-names All
{
  "Messages": [{
    "MessageId": "8f3c1e2a-4b7d-11ef-9c2a-0242ac120002",
    "Attributes": { "ApproximateReceiveCount": "4" },
    "Body": "{\"orderId\":\"A-4821\"}"
  }]
}

Choosing maxReceiveCount

ValueFits
1, 2Deterministic failures: malformed payloads, validation errors
3, 5The sensible default for most workloads
10+Consumers depending on a flaky downstream that recovers on its own

The trade is between recovering from transient problems and finding out quickly that something is broken. Set it to 1 and a brief downstream blip dead-letters your traffic; set it to 50 and a genuinely malformed message is retried for half an hour while you wonder why throughput dropped.

If you are retrying largely to ride out downstream outages, prefer a shorter count plus a real backoff strategy. A count of 5 with no delay between attempts exhausts itself in seconds, which is not a retry policy so much as a rapid giving-up.

A DLQ nobody watches is worse than no DLQ

Without a DLQ, a failing message is loud: it retries endlessly, floods logs and shows up in your error rate. With a DLQ, it disappears quietly into another queue. That is an improvement only if something tells you.

aws cloudwatch put-metric-alarm \
  --alarm-name orders-dlq-not-empty \
  --namespace AWS/SQS \
  --metric-name ApproximateNumberOfMessagesVisible \
  --dimensions Name=QueueName,Value=orders-dlq \
  --statistic Maximum --period 300 --evaluation-periods 1 \
  --threshold 0 --comparison-operator GreaterThanThreshold \
  --alarm-actions "$SNS_TOPIC_ARN"

Threshold zero, not ten. Any message in a DLQ means something failed five times in a row; there is no acceptable background level. Create this alarm in the same change that creates the DLQ, not as a follow-up ticket.

Inspecting without consuming

You want to read a dead-lettered message without starting its clock again. Use a long visibility timeout so it stays put while you look:

aws sqs receive-message --queue-url "$DLQ_URL" \
  --max-number-of-messages 10 \
  --visibility-timeout 0 \
  --attribute-names All

--visibility-timeout 0 makes the message immediately available again, which is what you want while investigating. You are reading, not claiming. The SentTimestamp and ApproximateFirstReceiveTimestamp attributes tell you when the message was created and when it first failed, which usually pins the failure to a deployment.

Redriving messages back

Once the bug is fixed, move the messages back to the source queue. The managed redrive does this without a script:

aws sqs start-message-move-task \
  --source-arn "$DLQ_ARN" \
  --destination-arn "$SRC_ARN" \
  --max-number-of-messages-per-second 50
aws sqs list-message-move-tasks --source-arn "$DLQ_ARN"
{
  "Results": [{
    "Status": "COMPLETED",
    "ApproximateNumberOfMessagesMoved": 1284,
    "MaxNumberOfMessagesPerSecond": 50
  }]
}

Rate-limit it. Redriving a large backlog at full speed onto a service that has just recovered is a reliable way to knock it over a second time.

Deploy the fix first, and confirm it works on one message before moving thousands. The destination queue is a normal queue, so a still-broken consumer simply dead-letters everything again, this time with the receive count reset.

For a FIFO source you may also need a RedriveAllowPolicy on the DLQ naming which queues are permitted to use it:

aws sqs set-queue-attributes --queue-url "$DLQ_URL" --attributes '{
  "RedriveAllowPolicy": "{\"redrivePermission\":\"byQueue\",\"sourceQueueArns\":[\"arn:aws:sqs:eu-west-1:123456789012:orders\"]}"
}'

Do not consume the DLQ automatically

A tempting pattern is a second worker reading the DLQ and retrying. Resist it: you have rebuilt the infinite retry loop the DLQ existed to break, with an extra queue in the middle.

The DLQ is for human attention. What belongs there is an alarm, a dashboard, and enough message metadata to diagnose the failure. Automated handling is reasonable only when it is doing something genuinely different, archiving the payload to S3 for later analysis, or raising a ticket, rather than reprocessing.

Common errors

InvalidParameterValue: Value {...} for parameter RedrivePolicy is invalid.
Reason: Dead letter target owner should be same as the source.

The DLQ is in a different account or region. Both must match the source queue.

InvalidParameterValue: Value {...} for parameter RedrivePolicy is invalid.
Reason: Dead letter target queue type should be the same as the source.

A FIFO source with a Standard DLQ, or the reverse.

ResourceNotFoundException: The specified queue does not exist

Usually the ARN was built by hand. Read it from get-queue-attributes --attribute-names QueueArn rather than assembling the string.

Quick reference

TaskCommand
Attach a DLQaws sqs set-queue-attributes --attributes RedrivePolicy=…
Read the DLQ ARNaws sqs get-queue-attributes --attribute-names QueueArn
Inspect without claimingaws sqs receive-message --visibility-timeout 0
See attempt count--attribute-names ApproximateReceiveCount
Redrive backaws sqs start-message-move-task
Check redrive progressaws sqs list-message-move-tasks
Stop a redriveaws sqs cancel-message-move-task
Detach a DLQSet RedrivePolicy to ""

Key takeaways

  • The redrive policy lives on the source queue, not on the DLQ.
  • Set the DLQ's retention to 14 days. The 4-day default loses evidence over a long weekend.
  • SQS counts receives, not failures: a short visibility timeout dead-letters healthy messages.
  • 3 to 5 is the sensible maxReceiveCount for most workloads.
  • Alarm on depth greater than zero, in the same change that creates the DLQ.
  • Inspect with --visibility-timeout 0 so reading a message does not consume an attempt.
  • Fix, verify on one message, then redrive with a rate limit.
  • Never auto-consume a DLQ. That recreates the loop it was built to break.

Check your understanding

Exercise

Good messages in the DLQ

A DLQ is filling with messages that process perfectly when replayed by hand. The queue has maxReceiveCount 5, and the consumer takes around 50 seconds per message. What should you check first?

Exercise

Automating DLQ recovery

To avoid manual work, a team adds a worker that reads the DLQ and sends each message back to the source queue immediately. Why is this a poor design?


Frequently Asked Questions

What is an SQS dead-letter queue and why do I need one?

It is a normal queue that receives messages which have failed processing too many times, configured through a redrive policy on the source queue. Without one, a message that cannot be processed is retried indefinitely. Burning consumer capacity, filling logs with the same error, and on a FIFO queue blocking every other message in its group. The DLQ moves that message aside so healthy traffic continues and the failure becomes something you can investigate rather than something that degrades the system.

How do I choose maxReceiveCount?

Three to five suits most workloads. Use one or two when failures are deterministic, a malformed payload will not succeed on the fourth attempt either, and ten or more when your consumer depends on a downstream service that fails transiently and recovers by itself. Higher values delay discovery and let bad messages consume capacity for longer; lower values dead-letter traffic during brief blips. If you are retrying mainly to survive outages, add real backoff rather than simply raising the count.

Why are perfectly valid messages ending up in my dead-letter queue?

Because SQS counts deliveries, not failures. If the visibility timeout is shorter than your processing time, each message is handed out again while the previous consumer is still working on it, and the receive count climbs past maxReceiveCount even though the work succeeds every time. This is the single most common cause of a confusing DLQ. Compare your handler's p99 duration with the queue's VisibilityTimeout before looking anywhere else.

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

Use the managed redrive: aws sqs start-message-move-task --source-arn "$DLQ_ARN" --destination-arn "$SRC_ARN", with --max-number-of-messages-per-second to rate-limit it. Deploy the fix first and test it against a single message, because the destination is an ordinary queue and a still-broken consumer will simply dead-letter everything again. Rate limiting matters too. Redriving a large backlog at full speed at a service that has just recovered tends to take it down again.

Should something consume the dead-letter queue automatically?

Not to reprocess. Sending messages straight back rebuilds the infinite retry loop the DLQ was created to break, and hides the failure because the queue keeps draining and never trips an alarm. Automation is reasonable when it does something else: archiving payloads to S3 for analysis, emitting a metric, or opening a ticket. Redrive should be a deliberate action taken after a fix is deployed.

What retention period should a dead-letter queue have?

The maximum, 14 days (MessageRetentionPeriod=1209600). The 4-day default is easy to exceed. A failure on Friday evening can expire before anyone looks on Tuesday, taking the only record of what went wrong with it. There is no cost argument against it, since you are charged for requests and storage volume rather than retention window, and a DLQ should be nearly empty in normal operation anyway.

Continue Learning

Explore Related Topics

When it breaks

Related Resources