SQS Dead-Letter Queue Filling Up? Redrive Messages Safely

Logeshwaran
—

The direct fix: open the queue you've set up as a dead-letter queue in the SQS console, choose Start DLQ redrive, pick "Redrive to source queue(s)," and let Amazon SQS move the messages back for you — it will not delete anything in the process. But here's the part almost nobody checks first: those messages haven't been sitting safely in storage while they waited. Amazon SQS counts a standard queue's retention period from the moment the message was originally sent, not from when it landed in the dead-letter queue, so a backlog that "looks fine" can be quietly closer to permanent deletion than anyone realizes.

⚡ Quick Answer

• Console fix → Open the DLQ → Start DLQ redrive → choose destination → choose velocity → Redrive messages

• Programmatic fix → call StartMessageMoveTask from the AWS CLI or SDK

Neither one deletes a single message — but neither one fixes why messages ended up there either. Check the retention trap before you redrive anything, then jump to the full console steps or the CLI/SDK steps.

Jake found his dead-letter queue the hard way. A customer called asking why her repair-status text never arrived, and by the time Jake dug into it, the queue holding those notifications had over four thousand messages sitting in a DLQ nobody on his three-person team had looked at in six weeks.

"So do I just... move them all back?" he asked. "Won't that just fail again and dump them right back where they started?"

That's the right instinct, and it's exactly why this guide covers both halves of the problem: how to redrive the backlog you already have without losing a single message, and how to make sure you're not just recycling the same failure in a loop. We'll walk through the console method, the API method, the permissions that trip people up, the FIFO-specific rules, the cost and privacy angles, and the monitoring that stops this from becoming a recurring 11pm phone call.

What it means when a dead-letter queue is filling up

A dead-letter queue, or DLQ, is a second SQS queue that catches messages your normal queue — the "source queue" — couldn't get processed. You set a limit called maxReceiveCount on the source queue: the number of times a consumer (your application code, a Lambda function, whatever is pulling messages off the queue) is allowed to pick up a message and fail to delete it before Amazon SQS gives up and reroutes that message to the DLQ instead. If your consumer keeps crashing, timing out, or throwing an exception on a particular message, that message eventually gets shipped off to the DLQ rather than looping forever in the main queue.

That's the whole point of a DLQ — it isolates the messages nobody could process, so the healthy ones keep flowing and you get a place to go inspect the ones that didn't. A DLQ "filling up" just means messages are failing faster than anyone is looking at the DLQ. It isn't itself an error state. It's a warning light. The question is what's behind the light.

Three different reasons a DLQ fills up — and they need three different fixes

Before you touch the redrive button, figure out which of these you're actually looking at:

  1. A one-time bad batch. A bug shipped, broke message processing for a few hours, got fixed, and now there's a pile of messages from that window sitting in the DLQ. These are safe to redrive once the fix is deployed — they'll process normally this time.
  2. A poison-pill pattern. A small number of malformed or unusual messages will never process successfully no matter how many times you redrive them — bad JSON, a missing field, a payload your code doesn't know how to handle. Redriving these without changing the code just moves them back to the DLQ again.
  3. A capacity or configuration problem. Your consumer is too slow, your visibility timeout is shorter than your actual processing time, or a downstream service (a database, an API you call) is throttling or down. Every message technically "fails," even though nothing is wrong with the message itself.

Jake's case, it turned out, was the third kind. His Lambda function was calling a third-party SMS provider that had started rate-limiting his account, and every retry burned through the receive count before the provider's queue cleared. Redriving those messages without raising his function's timeout and adding backoff would have just refilled the DLQ within the hour.

🙋‍♂️ Jake's Reality Check

"Okay, but how do I even know which of the three it is? I don't have time to read four thousand messages one at a time."

You don't have to. Pull the message attributes on a handful of the oldest and newest messages in the DLQ, and CloudWatch Logs from your consumer around the time they failed. If the errors are all the same exception at the same rough time, that's a bad-batch or capacity problem. If the errors vary message to message, you're looking at poison pills mixed into the pile, and you'll need to sort those out before a bulk redrive, not after.

📚 READ THESE FIRST

Five short reads that make everything below click into place:

⚡ Two minutes each. Come back here when they are done.

The retention trap: your backlog is older than it looks

This is the counterintuitive part, and it's the first thing to check before you decide there's no rush. For a standard queue, Amazon SQS bases a message's expiration on when it was originally sent to the source queue — not on when it arrived in the dead-letter queue. When a message moves to the DLQ, its enqueue timestamp doesn't reset.

Say a message sits in your source queue for a day before it exhausts its receive count and gets moved to the DLQ. If your DLQ's retention period is set to the default 4 days, that message only has 3 days left before Amazon SQS deletes it permanently — not 4. The ApproximateAgeOfOldestMessage metric on the DLQ tells you how long it's been sitting in the DLQ specifically, which is a different number from how old the message actually is.

FIFO queues work differently here — the enqueue timestamp resets when a message moves into a FIFO dead-letter queue, so the same message gets the DLQ's full retention window starting fresh. That distinction only applies to FIFO; if you're running standard queues, don't assume it applies to you.

⚠️ What this actually breaks

If your DLQ's retention period is set equal to or shorter than your source queue's, a message can be silently deleted before you ever see it, with no error, no alert, and no way to recover it — because message retention deletion isn't logged as a failure, it's just Amazon SQS doing what a queue is supposed to do. Always set the dead-letter queue's retention period longer than the source queue's, and the maximum you can set on either one is 14 days (1,209,600 seconds). If you've never touched this setting, both queues are almost certainly still on the 4-day default.

Setting Default Range Recommended for a DLQ
Message retention period 4 days (345,600 sec) 60 sec – 14 days (1,209,600 sec) Longer than the source queue's — 14 days is safest for a DLQ you might not check daily
Visibility timeout 30 sec 0 sec – 12 hours Longer than your actual worst-case processing time on the source queue
maxReceiveCount None until you set one 1 – 1,000 High enough to survive a brief blip, low enough to catch real failures — most teams land between 3 and 5

How to actually look inside the DLQ before redriving

Before you redrive anything, it helps to actually look inside a few of the messages sitting in the DLQ rather than guessing from the error rate alone. In the SQS console, open the DLQ and use Send and receive messages, then Poll for messages, to pull a small sample without deleting them — polling from the console uses a short visibility timeout and returns the messages to the queue afterward. Look at the message body itself for obvious malformation, and check the message attributes panel for ApproximateReceiveCount, which tells you exactly how many times a consumer already tried and failed on that specific message, and SentTimestamp, which tells you how old it actually is regardless of when it landed in the DLQ. From the CLI, the equivalent is a receive-message call against the DLQ's URL with attribute names set to All, which returns the same receive count and timestamps in a form you can script against if you're triaging thousands of messages instead of a handful by eye.

Doing this for even ten or fifteen messages, spread across the oldest and newest in the queue, is usually enough to tell you which of the three failure patterns from earlier you're dealing with. A receive count that's identical across every sampled message and clusters right at your maxReceiveCount value points to a capacity or timeout problem, since everything failed the same number of times in the same window. A receive count that varies wildly — some messages failed once, others the full maxReceiveCount — points toward a handful of genuinely broken messages mixed in with otherwise-healthy ones that just got unlucky.

Large payloads are worth a specific mention here too, since they show up in DLQs more often than people expect. Amazon SQS caps a message at 1 MiB, and if your producer is trying to send something larger without using the Amazon SQS Extended Client Library, which stores the actual payload in S3 and sends only a reference through SQS, the send fails before the message ever reaches the queue at all. That's not a DLQ problem — it never gets that far — but it's worth ruling out if you're seeing send-side errors alongside your DLQ backlog rather than assuming everything traces back to the same consumer-side cause.

Redriving from the SQS console (the fastest fix)

For a one-off cleanup, or the first time you're doing this, the console is the least error-prone route. Amazon SQS runs this as a background task called a message move task — you kick it off and it works through the backlog on its own; you don't have to sit there watching a progress bar.

  1. Open the SQS console and go to Queues.
  2. Choose the queue that's currently set up as a dead-letter queue — the one with the backlog, not the source queue.
  3. Choose Start DLQ redrive.
  4. Under Redrive configuration, pick a message destination: Redrive to source queue(s) sends messages back where they came from; Redrive to custom destination lets you send them to a different queue instead, as long as it's the same type (standard-to-standard or FIFO-to-FIFO).
  5. Under Velocity control settings, choose System optimized to let SQS move messages as fast as it safely can, or Custom max velocity to cap it at a specific rate, up to 500 messages per second.
  6. Choose Redrive messages to start the task.

That's it — no messages are deleted at any point in this flow. Amazon SQS receives each message from the DLQ, sends a copy to the destination queue, and only then removes the original from the DLQ. If anything interrupts the task partway through, whatever already made it to the destination stays there.

✅ Why this is the one to use for a first cleanup

Start with Custom max velocity set low, not "System optimized," the first time you redrive a large backlog after fixing a bug. If the root cause fix didn't fully work, a slow drip back into the source queue lets you catch it in CloudWatch before your consumer gets hammered with thousands of messages that just fail all over again. Ramp the rate up once you've confirmed a batch is processing cleanly.

Redriving with the API or CLI (for automation)

If you want redrive to be part of a runbook, a Lambda function, or a CI step rather than a person clicking buttons, Amazon SQS exposes the same capability as three API actions: StartMessageMoveTask, ListMessageMoveTasks, and CancelMessageMoveTask. From the AWS CLI, starting a redrive back to the source queue looks like this:

aws sqs start-message-move-task \
  --source-arn arn:aws:sqs:us-east-1:123456789012:orders-dlq \
  --max-number-of-messages-per-second 50

Leave off --destination-arn and it redrives to whatever queue is configured as the source queue for that DLQ. Add it and point it at a different queue ARN to send messages somewhere else instead — useful if you want to route the backlog to a staging queue for a closer look before it touches production traffic again. Check on progress at any time:

aws sqs list-message-move-tasks \
  --source-arn arn:aws:sqs:us-east-1:123456789012:orders-dlq

And if something looks wrong mid-task — the source queue's error rate is spiking, say — cancel it:

aws sqs cancel-message-move-task \
  --task-handle "AQEB6nR2..."

Canceling doesn't undo anything that already moved — whatever made it to the destination queue before you canceled stays there. It just stops the task from moving any more.

🕐 What changed between versions

  • Before June 2023: redrive was console-only. Automating it meant writing your own receive-and-resend loop, which risked duplicate deliveries if it crashed mid-batch.
  • Since June 2023: StartMessageMoveTask, ListMessageMoveTasks, and CancelMessageMoveTask made redrive a native, resumable-by-AWS operation via SDK and CLI.
  • Since November 2023: the same redrive capability extended to FIFO dead-letter queues, which weren't supported at first launch.

Redrive velocity: why "as fast as possible" isn't always the right choice

Ethan's take on this one is blunt: "System optimized is the setting people reach for because it's the default-looking option, and it's the one most likely to make your afternoon worse."

Here's why. System optimized moves messages at the maximum rate SQS can manage. If your root-cause fix was incomplete — you patched the bug that caused half the failures but missed the other cause — System optimized will slam your source queue with the entire backlog in a burst, and your consumer (especially if it's Lambda scaling up from zero) can get overwhelmed just as fast as the DLQ filled up in the first place, except now it's happening to your primary queue instead of a queue nobody was watching.

Custom max velocity caps the rate, up to 500 messages per second, and lets you ramp it up gradually while watching your consumer's error rate and the source queue's ApproximateNumberOfMessagesVisible metric. Start low. If nothing spikes after a few minutes, increase it. If you see the same errors reappearing in your logs, cancel the task and stop before you've undone your own fix.

FIFO dead-letter queues: what's different

FIFO queues (the ones that guarantee order and exactly-once processing) can have dead-letter queues too, and they can be redriven the same three ways — console, CLI, SDK — but a few things behave differently.

A FIFO source queue must use a FIFO DLQ. You can't mix types; a standard queue can only use a standard DLQ and a FIFO queue can only use a FIFO DLQ. When Amazon SQS moves a message from a FIFO queue into a FIFO DLQ, it replaces the message's original deduplication ID with the message's own ID. That's deliberate — it stops the DLQ's deduplication window from silently dropping two unrelated messages that happened to share a dedup ID.

⚠️ What this actually breaks

Redrive preserves the order messages sit in inside the DLQ — oldest first — but the destination queue doesn't only receive redriven messages. If producers are still actively sending new messages to that FIFO queue while a redrive task is running, the redriven messages interleave with the new ones in whatever order they happen to arrive. If your application depends on strict ordering (Amazon's example is an edit list for video editing, where reordering steps changes the meaning of every step after it), a DLQ isn't a safe place to route those messages to begin with — redriving them back won't restore the original sequence.

The IAM permissions redrive actually needs

This is where most "Access Denied" errors on a first redrive attempt come from — people assume having queue access is enough, and it isn't. Redrive touches both the DLQ and the destination queue, and each needs its own permissions.

To do this You need, on the DLQ You also need
Start a redrive StartMessageMoveTask, ReceiveMessage, DeleteMessage, GetQueueAttributes SendMessage on the destination queue; kms:Decrypt/kms:GenerateDataKey if either queue is encrypted
Cancel a redrive CancelMessageMoveTask, ReceiveMessage, DeleteMessage, GetQueueAttributes kms:Decrypt if the DLQ is encrypted
Check redrive status ListMessageMoveTasks, GetQueueAttributes —

If your queues sit behind KMS encryption (SSE), both the DLQ's key and the destination's key need to be reachable by whoever's running the redrive — missing kms:Decrypt on the source key or kms:GenerateDataKey on the destination key is a common way this fails silently with a permissions error that doesn't obviously mention KMS at all.

Fix the cause before you redrive, or you'll be back here next week

"What if I just raise maxReceiveCount to something huge, like a thousand?" Jake asked. "Then nothing ever hits the DLQ again."

Ethan didn't hesitate. "That's the worst fix on this whole list. You haven't stopped anything from failing — you've just made the failures invisible. The message keeps getting redelivered forever, your consumer keeps burning compute retrying it, and you lose the one signal that told you something was wrong in the first place."

A DLQ is diagnostic information. The actual fixes, cheapest to most involved, look like this:

  • Lengthen the visibility timeout if your consumer's processing time is close to or longer than the current timeout. Jake's own function needed close to 45 seconds once it had to call the SMS provider, wait for a response, and retry once on a timeout — but his queue's visibility timeout was still sitting at the 30-second default from when he first created it. That gap alone was enough for a second invocation to pick the message up while the first was still working; both would eventually try to delete it, one would fail, and the receive count crept toward maxReceiveCount on messages that were never actually broken.
  • Add retry backoff and error handling in your consumer code for transient failures (a downstream API being briefly rate-limited, a database connection blip) so a temporary hiccup doesn't count against maxReceiveCount at all.
  • Fix or reject the poison-pill messages specifically — if a handful of messages have a malformed payload your code can't parse, don't redrive those particular ones; pull them out, log what's wrong with them, and decide separately whether to fix and resend or discard them.
  • Scale or unblock the downstream dependency if the real problem is a database, another queue, or a third-party API that can't keep up — the messages were never the problem.

Only after one of these is actually in place does a redrive mean anything. Otherwise you're just adding a delay before the DLQ fills back up.

Redrive allow policy: controlling who can use your DLQ

This is a separate setting from maxReceiveCount, and it lives on the DLQ itself rather than the source queue. The redrive allow policy decides which source queues are permitted to point at this queue as their dead-letter queue at all. By default, every queue in the account and Region can use it — the setting is allowAll.

You can tighten that in two ways: set it to byQueue and list up to 10 specific source queue ARNs that are allowed to target this DLQ, or set it to denyAll to stop it being used as a DLQ entirely. This matters most in shared accounts, where you don't want a queue meant for one team's failures quietly becoming the catch-all for a queue someone else set up without asking.

Making sure you catch it next time, not six weeks late

Jake's real problem wasn't the redrive — it was that nobody looked at the DLQ for six weeks. That's a monitoring gap, and it's the actual fix, not the redrive button.

Set a CloudWatch alarm on the DLQ's ApproximateNumberOfMessagesVisible metric — trigger it any time that number goes above zero, or above whatever small number represents "one or two edge-case failures" for your workload rather than "something is systematically broken." Route the alarm to wherever your team actually looks — Slack, PagerDuty, an email alias someone reads — not just a CloudWatch dashboard nobody opens unless there's already a fire.

✅ Why this is the one habit worth building

An alarm that fires the same day a message hits the DLQ turns this into a five-minute investigation instead of a four-thousand-message backlog and a customer phone call. The cost of the alarm is nearly nothing; the cost of not having one is exactly what happened to Jake.

Building the alarm, step by step

If you'd rather set this up by hand in the console before committing it to code, here's the sequence:

  1. Open CloudWatch, go to Alarms, and choose Create alarm.
  2. Under Select metric, choose SQS, then find your DLQ's ApproximateNumberOfMessagesVisible metric.
  3. Set the condition to Greater than a threshold of 0 for standard traffic, or a small number like 5 if your workload has occasional expected edge-case failures that aren't worth an alert.
  4. Set the evaluation period short — one data point over one 1-minute period is enough for a DLQ, since you want to know the moment something lands there, not after it's had time to accumulate.
  5. Under Notification, point the alarm at an SNS topic that's actually wired to Slack, email, or PagerDuty — not a topic nobody subscribed to.
  6. Name it something searchable, like orders-dlq-has-messages, so six weeks from now whoever sees the alert knows exactly which queue and doesn't have to go hunting.

For teams already comfortable with Lambda, a lightweight self-healing pattern goes one step further than an alert: a scheduled function that checks the DLQ depth and, only for failure types you've already confirmed are safe to retry automatically, kicks off a conservative redrive without waiting for a human to click anything.

import boto3

sqs = boto3.client("sqs")
DLQ_ARN = "arn:aws:sqs:us-east-1:123456789012:orders-dlq"
DEPTH_THRESHOLD = 50

def handler(event, context):
    attrs = sqs.get_queue_attributes(
        QueueUrl=dlq_url_from_arn(DLQ_ARN),
        AttributeNames=["ApproximateNumberOfMessages"]
    )
    depth = int(attrs["Attributes"]["ApproximateNumberOfMessages"])

    if depth == 0:
        return {"status": "empty"}

    existing = sqs.list_message_move_tasks(SourceArn=DLQ_ARN)
    if any(t["Status"] == "RUNNING" for t in existing.get("Results", [])):
        return {"status": "already redriving"}

    if depth < DEPTH_THRESHOLD:
        return {"status": "below threshold, leaving for manual review"}

    sqs.start_message_move_task(
        SourceArn=DLQ_ARN,
        MaxNumberOfMessagesPerSecond=20
    )
    return {"status": "redrive started"}

Notice the guardrails baked in: it checks whether a task is already running before starting another one, it caps velocity deliberately rather than defaulting to System optimized, and it does nothing at all below a depth threshold, so a single stray failure doesn't trigger an automatic retry loop before a human has even seen it. Automating the redrive itself is the easy part; automating it safely is mostly about what the function refuses to do. Schedule it the way you'd schedule anything else that needs to run unattended, on a fixed interval that someone actually remembers exists — a scheduled job nobody remembers setting up is exactly the kind of thing that quietly stops working the same way a queue nobody watches quietly fills up, and the fix in both cases is the same: put it somewhere a human will notice if it goes quiet.

Automating redrive with EventBridge, and the limits to know

Once you trust that a given class of failure is transient and safe to auto-retry, you can wire an EventBridge scheduled rule to invoke the Lambda function above on a timer rather than run it by hand. Keep this narrow, though — automatic redrive is appropriate for "this fails occasionally under load and always succeeds on the second try" workloads, and dangerous for "this fails because the message itself is bad" workloads, where an automated loop just burns the same message against your consumer over and over on a schedule instead of a human doing it once by mistake.

A redrive task can run for a maximum of 36 hours, and an account can have up to 100 active redrive tasks running at once — more than enough headroom for most teams, but worth knowing if you're kicking off automated redrives across many queues at the same time.

What letting the backlog sit actually costs you

The Amazon SQS Free Tier covers 1 million requests a month, and every API action beyond that — including the ReceiveMessage and DeleteMessage calls a redrive task performs behind the scenes — is billed per request once you're past it. For almost every team, that's not where the real cost of a filling DLQ shows up. A redrive of a few thousand messages is a rounding error on an AWS bill; the free tier alone usually absorbs it without you noticing.

The real cost is everywhere else. It's the engineering hour spent reconstructing what happened after nobody noticed for six weeks. It's the customer who calls instead of getting the text she was promised. It's the compute a Lambda function quietly burns retrying the same poison-pill message hundreds of times before someone finally sets a sane maxReceiveCount. None of that shows up as a line item on the bill, which is exactly why it's so easy to under-invest in the one alarm that would have caught it on day one instead of week six.

🙋‍♂️ Jake's Reality Check

"So the redrive itself basically costs nothing. It's the not-noticing that cost me a customer."

Exactly that. The button is free. The silence before you press it is the expensive part.

The privacy check before anyone opens that DLQ

Before you widen who on the team can inspect DLQ contents to debug a backlog, take a look at what's actually inside those messages. Order confirmations, repair tickets, and notification payloads routinely carry a customer's name, phone number, or order details in plain text in the message body. Amazon SQS itself does nothing to redact or flag that content — it's just a queue, and it doesn't know what's inside the payload any more than a delivery truck knows what's in the boxes it's carrying.

That matters most in two moments: when someone pastes a raw message body into a support ticket or a Slack thread to ask "does this look right?", and when IAM permissions for the DLQ get handed out more broadly than the source queue's ever were, on the assumption that a dead-letter queue is somehow lower-stakes. It isn't. It holds the same customer data the source queue does, sitting in one place, already flagged as the batch someone is likely to go digging through.

⚠️ What this actually breaks

If your source queue's IAM policy is scoped narrowly and the DLQ's isn't, or if the redrive permissions table above got granted to a wider group "just to debug this one time," you've quietly created a second, less-guarded copy of the same customer data. Scope DLQ access to the same people who can already see the source queue, and redact message bodies before pasting them anywhere outside your own account, the same way you would with a database export. If your DLQ needs to be inspected by a wider group for an ongoing investigation, treat that access as temporary and time-boxed rather than a permanent widening of the IAM policy, and remove it once the investigation closes.

Do you need a third-party tool, or is the console enough?

A quick search for "SQS DLQ management" turns up a handful of paid observability platforms and open-source dashboards that promise a nicer view of your dead-letter queues than the AWS console gives you. Before you add another tool to the stack, it's worth being honest about what they actually add.

Most of them are doing one of two things: aggregating CloudWatch metrics from multiple queues into a single dashboard, or wrapping the same StartMessageMoveTask API in a friendlier UI with saved filters and history. If you're running a handful of queues in one account, neither of those buys you much over a CloudWatch dashboard and a couple of saved AWS CLI commands — the SQS console redrive flow already does everything a third-party wrapper does, and it does it without another vendor touching your message contents.

Where a third-party tool genuinely earns its place is scale: dozens of teams, dozens of accounts, and a need for one shared view of "which DLQs across the organization have messages older than X" without someone writing a cross-account CloudWatch query by hand. If you're at that scale, look for a tool that reads CloudWatch metrics and calls the native SQS APIs rather than one that requires routing your actual message traffic through a third-party service — the latter adds a new place your customer data lives, for a convenience a script could usually cover.

Setting this up as code, so it doesn't happen again

Clicking through the console works fine for a one-time cleanup. But if this queue matters enough to have a DLQ at all, the retention period, the redrive allow policy, and the alarm that watches it belong in infrastructure as code, not in whatever someone remembers clicking six months ago. A Terraform config for the source queue, its DLQ, and the redrive allow policy looks like this:

resource "aws_sqs_queue" "orders_dlq" {
  name                      = "orders-dlq"
  message_retention_seconds = 1209600 # 14 days - longer than the source queue
}

resource "aws_sqs_queue" "orders" {
  name                       = "orders-queue"
  visibility_timeout_seconds = 60
  message_retention_seconds  = 345600 # 4 days
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.orders_dlq.arn
    maxReceiveCount     = 5
  })
}

resource "aws_sqs_queue_redrive_allow_policy" "orders_dlq_allow" {
  queue_url = aws_sqs_queue.orders_dlq.id
  redrive_allow_policy = jsonencode({
    redrivePermission = "byQueue"
    sourceQueueArns   = [aws_sqs_queue.orders.arn]
  })
}

Applying that gives you three things at once, permanently, instead of relying on someone remembering to set them by hand: a DLQ retention period longer than the source queue's (closing the retention trap from earlier), an allow policy that limits which queues can dump into this specific DLQ, and a maxReceiveCount that's actually documented in version control instead of buried in a console setting nobody wrote down.

  1. Write the source queue and DLQ resources with an explicit, longer retention period on the DLQ.
  2. Add the redrive allow policy scoped to the specific source queue ARNs that should be allowed to use it.
  3. Add a CloudWatch metric alarm on the DLQ's ApproximateNumberOfMessagesVisible in the same file, so the alarm ships with the queue instead of being a separate, easy-to-forget step.
  4. Run a plan before an apply on any existing production queue — changing message_retention_seconds or the redrive policy on a live queue doesn't move existing messages, but it's still worth confirming nothing else in the plan touches queue names or ARNs that other services depend on.
Approach Best for Downside
Console redrive A one-off cleanup after a bug is fixed Nothing is recorded anywhere — the next person has no idea it happened
CLI / SDK redrive Repeatable redrives, runbooks, scripted incident response Still a manual trigger someone has to remember to run
Terraform / CloudFormation / CDK for the queue config Retention periods, allow policies, and alarms that shouldn't drift or get forgotten Overkill for a queue you spun up to test something for an afternoon

✅ Why this is the one to actually commit to

None of this stops messages from failing. It stops the failure from going unnoticed for six weeks, which was the actual problem in Jake's case, not the redrive itself.

Edge cases: VPC endpoints, shared DLQs, and cross-account limits

If your queues restrict access to a specific VPC using an aws:sourceVpc condition in the queue policy, redrive will fail unless you carve out an exception, because the redrive operation itself runs as an Amazon SQS service call from outside your VPC, not from inside it. Add an aws:CalledViaLast condition (or alternatively aws:ViaAWSService) to both the source queue's and the DLQ's policy so SQS is allowed to make the call on your behalf while direct access from outside the VPC stays blocked.

On shared DLQs: a single dead-letter queue can be the target for more than one source queue, and that's a supported pattern. It just means when you redrive, you're redriving the whole DLQ's backlog back toward whichever queue(s) originally sent each message — Amazon SQS tracks that per message, so a shared DLQ doesn't require you to sort messages by source queue yourself. One thing that isn't supported at all: a DLQ has to sit in the same AWS account and the same Region as its source queue. If you're running a multi-account setup and hoping to centralize every team's dead letters into one shared account, that's not something Amazon SQS does natively — you'd need to redrive or forward messages across accounts yourself.

What it looks like when redrive itself goes wrong

A few failure signatures worth recognizing rather than guessing at:

  • "Access Denied" on StartMessageMoveTask — almost always a missing permission on either the DLQ or the destination queue, or a missing KMS permission if either is encrypted. Check the table above line by line rather than assuming it's a broad IAM problem.
  • The redrive finishes but the DLQ refills within minutes — the root cause wasn't actually fixed. Go back to the triage section and confirm whether you're dealing with a poison pill or a capacity issue before redriving again.
  • Messages you expected to redrive are missing afterward — check the DLQ's retention period against how long the message had already been alive in the source queue. This is the retention trap from earlier, and it's the single most common reason someone says "SQS lost my messages" when nothing actually malfunctioned.
  • Task shows RUNNING but nothing seems to move — give it a few minutes; large backlogs on System optimized can take time to ramp, and Custom max velocity set very low will look slow by design. Use ListMessageMoveTasks to confirm it's actually progressing rather than stalled.

And one thing SQS deliberately does not offer: there's no way to filter or edit messages while they're being redriven. Amazon SQS moves everything in the DLQ, unmodified, in the order it was received. If you need to keep some messages in the DLQ and only redrive others, you have to pull the ones you want to exclude out first (receive and delete them, or move them elsewhere), because a redrive task takes the whole queue as it stands.

What to do with the messages you don't redrive

Not every message in a DLQ deserves a redrive. Once you've sorted the poison pills from the legitimately retryable messages, you still have to decide what happens to the ones you're not sending back. Three honest options, in order of how often teams actually use them: leave them where they are until the retention period naturally expires, which is fine if you've already extracted whatever debugging value they had and don't need them for compliance; export the message bodies somewhere durable first — an S3 bucket or a data warehouse — if you need an audit trail of what failed and why before you let them go; or delete them outright, with DeleteMessage for a handful, or PurgeQueue for the whole queue at once.

PurgeQueue is worth using carefully: you cannot retrieve anything it deletes, the deletion itself can take up to 60 seconds to fully complete, and the API will reject a second purge request on the same queue within 60 seconds of the first one with a PurgeQueueInProgress error. It is not the tool for "let me clear out just the bad ones" — it clears everything, including any message that might arrive in that window while the purge is still running.

Frequently asked questions

What does it mean when my SQS dead-letter queue is filling up?

It means messages are exceeding the maxReceiveCount you set on the source queue before your consumer successfully deletes them, so Amazon SQS is rerouting them to the DLQ instead of leaving them looping in the main queue. It's a symptom, not the underlying problem — the cause is usually a bug, a capacity issue, or a handful of malformed messages.

Will redriving messages from a DLQ delete them if it fails?

No. A redrive task only removes a message from the DLQ after it has been successfully delivered to the destination queue. If the task is interrupted or canceled partway through, whatever hasn't been moved yet stays exactly where it was.

Does redriving a message reset its message ID?

Yes. Redriven messages are treated as new messages with a new message ID and a new enqueue time once they land in the destination queue. If your application logs or tracks messages by ID, expect that ID to change after a redrive.

How long do messages stay in a dead-letter queue before they're deleted?

Whatever the DLQ's message retention period is set to — anywhere from 60 seconds to 14 days, 4 days by default. For standard queues, that countdown started when the message was originally sent to the source queue, not when it arrived in the DLQ, so it may have less time left than you'd expect.

Can I redrive messages without fixing the root cause?

You can, but the same messages will likely fail the same way and land right back in the DLQ. Redrive moves messages; it doesn't change why they failed. Confirm the underlying issue is fixed first, or at least redrive at a slow, watched pace so you can cancel if the failures come back.

What's the difference between redriving to the source queue vs. a custom destination?

Redriving to the source queue sends messages back exactly where they came from. Redriving to a custom destination sends them to a different queue of the same type instead, which is useful if you want to review the backlog somewhere separate from live production traffic before it goes back into normal processing.

Can I filter which messages get redriven?

No, Amazon SQS doesn't support filtering or modifying messages during a redrive task — it moves everything in the DLQ. If you need to exclude specific messages, receive and remove them from the DLQ (or move them elsewhere) before starting the redrive.

Can I redrive messages from a FIFO dead-letter queue?

Yes, FIFO dead-letter queues support redrive the same way standard ones do, through the console, CLI, or SDK. The destination must also be a FIFO queue, and the deduplication ID on each message gets replaced with the message's own ID when it first enters the FIFO DLQ.

What happens if a message fails again after redrive?

It goes through the same receive-count cycle again on the source queue and, once it hits maxReceiveCount, gets moved back to the DLQ as a new message with a fresh ID and receive count. There's no built-in limit stopping a message from cycling this way repeatedly if the underlying cause is never fixed.

How many messages can I redrive at once, and is there a limit?

There's no cap on the number of messages a single redrive task can move — it works through the entire DLQ backlog. The limits that do apply are on the task itself: it can run for a maximum of 36 hours, and an account can have up to 100 active redrive tasks running at the same time.

How long can a redrive task run?

Up to 36 hours per task. For most backlogs this is far more time than needed, but it matters if you're moving an unusually large number of messages at a deliberately slow custom velocity.

Can I cancel a message redrive task partway through?

Yes, using CancelMessageMoveTask from the CLI/SDK or the "Cancel DLQ redrive" option in the console. Anything already moved to the destination queue before you cancel stays there; only messages not yet moved remain in the DLQ.

What IAM permissions do I need to redrive DLQ messages?

On the DLQ: StartMessageMoveTask, ReceiveMessage, DeleteMessage, and GetQueueAttributes. On the destination queue: SendMessage. If either queue is encrypted with KMS, you also need the relevant kms:Decrypt and kms:GenerateDataKey permissions on the respective keys.

Why is StartMessageMoveTask failing with an access denied error?

Almost always a missing permission somewhere in that set — commonly SendMessage on the destination queue being overlooked, or a missing KMS permission when the queues are encrypted. Check both queues' permissions individually rather than assuming it's one broad policy problem.

Should I just increase maxReceiveCount to stop messages going to the DLQ?

Only if the real problem is a brief, occasional blip that a couple of extra retries would ride out. Setting it very high to make the DLQ stop filling doesn't fix anything — it just hides failing messages inside endless retries on the main queue instead of surfacing them where you can see and fix the cause.

Can two different queues share the same dead-letter queue?

Yes, as long as the redrive allow policy on the DLQ permits it. By default every queue in the account can target a given queue as its DLQ; you can restrict this to specific source queue ARNs, or deny it entirely, using the redrive allow policy.

How do I get alerted before the DLQ fills up next time?

Set a CloudWatch alarm on the DLQ's ApproximateNumberOfMessagesVisible metric, triggered as soon as it rises above zero (or whatever small threshold fits your workload), and route it somewhere your team actually monitors. Catching the first failed message the day it happens beats discovering a multi-thousand-message backlog weeks later.

Jake's redrive, in the end, took about twenty minutes once he'd raised his Lambda timeout and added exponential backoff for the SMS provider's rate limit. He ran it at a custom velocity of 20 messages a second, watched the error rate stay flat, and bumped it up once he was sure. The customer got her text three weeks late. Not ideal — but not lost, either, which is the whole point of having a DLQ in the first place.

📖 ALSO READ

Hitting other AWS errors? These save your next 2 a.m.:

⚡ Bookmark this page. The list grows as new guides land.

Revision note. Written September 2026, covering the current SQS redrive APIs (StartMessageMoveTask, ListMessageMoveTasks, CancelMessageMoveTask), console redrive, and FIFO dead-letter queue support. This will need a refresh if AWS changes the 36-hour task limit, the 500-messages-per-second velocity cap, or the redrive allow policy's 10-queue limit. If you've been staring at a growing DLQ wondering whether you've already lost something, take a breath — nothing in that queue disappears until its retention period runs out, and now you know exactly how to check.

Related