Redriving a dead-letter queue (DLQ) is a production mutation, not a cleanup chore. A message in a DLQ means the broker stopped delivering it. It does not tell you whether the work never ran, ran partway, or finished while the acknowledgment was lost. Pressing a "redrive all" button before you know which case you are in is how a recoverable incident becomes duplicate charges, double-sent notifications, or one tenant's data landing in another tenant's workflow.
This article is written for DevOps leads, SREs, and senior developers who own queue-backed consumers. It covers why messages dead-letter, what native redrive features do and do not do, and a proposed recovery lifecycle you can adapt: quarantine, classify, repair, reconcile, release. It also covers the handler-side contracts that make replay safe: verified acknowledgment, terminal failure, uncertain-input handling, tenant-safe idempotency, and clear privacy ownership.
The vendor behavior cited here comes from the Amazon SQS dead-letter queue documentation and the Azure Service Bus dead-letter queue documentation. Treat provider limits as things to re-verify against current documentation before you rely on them. The code is illustrative, not an official SDK contract.
Key Takeaways (Bottom Line Up Front)
- A DLQ entry is a failed delivery, not a verified failure of the work. Delivery counters can increment because a lock expired or an acknowledgment was lost, even when the side effect succeeded.
- Native redrive moves messages; it does not make them safe. For example, Amazon SQS documents that redrive cannot filter or modify messages, and redriven messages receive a new message ID and enqueue time. Your idempotency key therefore cannot be the broker message ID.
- Bind each operation to a durable, tenant-scoped identity and an immutable payload hash. Reject a reused identity with a different payload, claim work atomically, and distinguish a legitimate later operation from a replay.
- Never blindly retry an ambiguous timeout on a consequential mutation. Record the outcome as UNKNOWN, reconcile against the authoritative system, then decide.
- Release through all-required go/no-go gates, not a summed score. A canary batch at a throttled rate comes before any bulk replay.
- DLQ payloads are data you own. They carry personal data and sensitive identifiers, need an owner, a retention decision, and a place in your erasure workflow.
Why Messages End Up in a Dead-Letter Queue, and Why Redrive Is Not Neutral
Key takeaway: A dead-letter queue records that a broker gave up delivering. It does not record why the work failed, and it cannot tell you whether the side effect already happened.
Brokers move a message to a DLQ when a delivery limit is exceeded or the message cannot be processed. In Amazon SQS, the redrive policy's maxReceiveCount defines how many times a consumer can receive a message before it is moved; the GetQueueAttributes reference lists a default of 10. In Azure Service Bus, the maximum delivery count also defaults to 10, and the delivery count increments when a peek-locked message is abandoned or its lock expires. Service Bus documents that this dead-lettering behavior cannot be disabled, only tuned with a larger limit.
That last detail matters for recovery. A handler that is slow, or whose connection drops before it settles a message, can push a message toward the DLQ even though the business operation completed. The Service Bus documentation explicitly describes messages that are repeatedly received but never settled ending up dead-lettered with the reason MaxDeliveryCountExceeded. So when you open a DLQ, you are looking at three possible histories for every message:
- Never executed: the payload was rejected before any side effect.
- Partially executed: some steps ran, others did not.
- Fully executed, acknowledgment lost: the work is done, and a replay would repeat it.
Only the handler's own durable record, or the target system's authoritative state, can distinguish these. The broker cannot.
Common causes and the right first action
| Cause | Typical signal | Safe first action |
|---|---|---|
| Unparseable or schema-invalid payload | Validation error on the same field across many messages | Quarantine. Do not replay until a repair path exists with recorded provenance. |
| Permanent business rejection | Domain error code such as a closed account | Record a terminal outcome and notify the owning team. Do not replay. |
| Dependency outage | Clustered timestamps, timeouts, 5xx from one downstream | Wait for dependency health, then canary replay at a throttled rate. |
| Consumer bug since fixed | One exception signature, one deploy window | Verify the fix on a canary batch, then replay with idempotency checks. |
| Lock or visibility timeout shorter than handler runtime | No application error, repeated redelivery | Fix timing first. Assume side effects may have completed and reconcile before replay. |
| Expired or superseded message | Old enqueue time, newer operation on the same entity | Check freshness against current entity state. Often terminal as stale. |
Observability is what makes this table usable. If your traces and logs cannot connect a dead-lettered message to the handler attempts that produced it, classification becomes guesswork. Our guide to distributed tracing and observability for low MTTR covers the correlation basics.
How Most Teams Handle DLQs, and Where Native Redrive Stops
Key takeaway: Most teams treat the DLQ as a backlog to drain. Native redrive tools are transport features: they preserve neither your idempotency key nor your tenant boundaries, and they cannot inspect what they move.
The common pattern is: an alarm fires on DLQ depth, someone finds the bug, the fix ships, and someone clicks "redrive." It feels responsible because the tool is built into the console. The failure is that the tool answers "how do I move bytes back" while the real question is "is it safe to run this work again, for this tenant, now."
What the documented behavior implies
| Documented behavior (vendor docs) | Engineering implication |
|---|---|
| SQS redrive does not support filtering or modifying messages. (SQS redrive documentation) | You cannot redrive only one tenant or only one error class. Selective replay needs your own redriver. |
| Redriven SQS messages are treated as new messages with a new message ID and enqueue time; the retention period resets. | The broker message ID is useless as an idempotency key. Use a business operation ID carried in the payload or attributes. |
| SQS redrive velocity can be capped; the console documents a maximum custom rate of 500 messages per second and recommends starting small. | The default "as fast as possible" mode can overwhelm a downstream that just recovered. Always set a rate. |
| A redrive task can run a maximum of 36 hours, with a limit of 100 active tasks per account. | Large backlogs need planning, and automation must check task status instead of assuming completion. |
| Redriven messages interweave with new producer traffic on the destination queue; the SQS guide also warns against using a DLQ with a FIFO queue if you need to preserve exact order. | Ordering guarantees do not survive recovery. Per-entity version checks must live in the handler or target system. |
| For SQS standard queues, a message keeps its original enqueue timestamp when moved to the DLQ, so a DLQ retention period shorter than the source's can delete it early; for FIFO queues the timestamp resets. | Set DLQ retention deliberately and monitor age. Silent expiry is a data-loss path. |
| Service Bus keeps dead-lettered messages until you explicitly retrieve and complete them, adds a dead-letter reason and description, does not observe time-to-live in the DLQ, and leaves resubmission to the application. (Service Bus DLQ documentation) | Reason codes are your classification input. Resubmission logic, including idempotency, is yours to build. |
2026 update: verify limits, keep the principles
The numeric limits above reflect the vendor documentation we reviewed in 2026. Managed messaging services change quotas, console behavior, and APIs (SQS, for example, exposes redrive as StartMessageMoveTask, ListMessageMoveTasks, and CancelMessageMoveTask). Re-check the current documentation before you encode any limit into a runbook. The durable principles do not depend on a vendor: redriven work gets a new transport identity, the transport cannot judge safety, and your handler must supply idempotency, tenant checks, and outcome records.
A Proposed Recovery Lifecycle: Quarantine, Classify, Repair, Reconcile, Release
Key takeaway: Separate the decision to replay from the mechanics of replay. Each stage has an exit criterion, and release requires every go/no-go control to pass.
The lifecycle below is a proposed design, not a standard and not a method we claim to be an industry norm. Adapt the stages to your broker and risk profile.
- Quarantine. Stop the bleeding: pause or cap the consumer if it is dead-lettering at scale, and make sure DLQ retention will outlast your investigation. Restrict who can read or move DLQ content.
- Classify. Group messages by failure cause using dead-letter reason, exception signature, tenant, operation type, and age. Do this read-only, and copy only minimal metadata (operation ID, tenant ID, reason, timestamps, payload hash) into a triage store so analysis does not spread payloads around.
- Repair. Fix the cause: deploy the consumer fix, correct the timeout, restore the dependency, or define a documented transformation for malformed payloads. A repaired payload is a new approved payload: give it a new payload hash and record provenance linking it to the original. Never mutate a message in place and reuse the same operation identity.
- Reconcile. For every message whose outcome could be UNKNOWN, query the authoritative system (payment provider, ledger, target database) and record what actually happened before deciding to replay. The reconcile-before-retry design walks through this pattern.
- Release. Replay a canary slice, observe, then ramp at a rate your downstream can absorb. Record each decision (replay, terminal reject, stale, manual) per operation.
Mandatory go/no-go controls before any bulk release
These are all-required checks. They are not a scoring model, and passing them does not certify that your system is production-ready in general; it only means this specific replay has met the minimum safety conditions.
| # | Control | No-go condition |
|---|---|---|
| 1 | Root cause identified and fix verified in the live path | Fix only tested in staging, or cause still a hypothesis |
| 2 | Every message carries a durable, tenant-scoped operation ID | Idempotency would depend on the broker message ID |
| 3 | Operation ledger binds the ID to an immutable payload hash and rejects changed-payload reuse | Same ID can be accepted with different content |
| 4 | Work is claimed atomically; concurrent workers cannot both execute | Read-then-write claim without a unique constraint or conditional update |
| 5 | Side effects use target-side conditional mutation or an idempotency key honored by the target | Target cannot dedupe and no per-entity serialization exists |
| 6 | UNKNOWN outcomes are reconciled against authoritative state before replay | Timeouts treated as failures and retried blindly |
| 7 | Stale or superseded operations are detected via entity version or timestamp checks | Replay could overwrite newer state |
| 8 | Replay rate is capped below measured downstream headroom, with a canary slice first | Unthrottled or "system optimized" bulk replay |
| 9 | A named owner can pause the replay, and the pause path was tested | No tested stop mechanism |
| 10 | DLQ data access, retention, and erasure scope are documented | Unknown who may read payloads or how long copies persist |
If any row is a no-go, you do not replay yet. You may still be able to resolve some messages as terminal or stale, which is often the safest and fastest way to shrink the queue.
Tenant-Safe Idempotency for Redriven Work
Key takeaway: Idempotency is a durable record, not a hope. Key it by tenant and business operation, bind it to the approved payload hash, and let the database or target enforce the claim.
In multi-tenant systems, an operation ID that is unique only within a tenant must never be checked globally without the tenant in the key, or one tenant's replay can collide with, or leak the existence of, another's. Use a composite identity: (tenant_id, operation_id). For background on the broader pattern, see our production-ready idempotency framework.
Requirements the ledger must satisfy
- Durable, tenant-scoped identity. Generated at the origin of the business intent (for example, when the user or upstream system submits the request), carried in the message body or attributes, and unchanged across redrive.
- Immutable approved payload hash. Hash a canonical serialization of the approved payload. If the same identity arrives with a different hash, reject it and quarantine for human review. Do not silently apply the new content and do not silently drop it.
- Atomic claim. A unique constraint plus a conditional update lets exactly one worker claim an operation. Check-then-insert in application code races under concurrency.
- Legitimate later operations remain possible. A user who really does submit a second, identical-looking request gets a new operation ID at the origin. Do not deduplicate on content alone, or you will suppress valid work.
- Target-side conditional mutation. A read, compare in application memory, then write sequence is not compare-and-swap. Use a conditional write on the target, such as an update guarded by an expected version, or a target-honored idempotency key. If the target supports neither, say so in the design record: you then need per-entity serialization (for example, one in-flight operation per tenant and entity) and must accept the conflict window that remains.
Illustrative ledger and claim (not an official API)
-- Illustrative schema. Adapt to your database. CREATE TABLE operation_ledger ( tenant_id text NOT NULL, operation_id text NOT NULL, -- business identity, NOT the broker message ID payload_hash bytea NOT NULL, -- hash of canonicalized approved payload state text NOT NULL, -- NEW | CLAIMED | SUCCEEDED | REJECTED_TERMINAL | UNKNOWN | STALE | QUARANTINED lease_owner text, lease_expires_at timestamptz, attempt int NOT NULL DEFAULT 0, outcome_ref text, -- pointer to authoritative result or reconcile record PRIMARY KEY (tenant_id, operation_id) ); -- 1) Register intent (idempotent): first writer wins. INSERT INTO operation_ledger (tenant_id, operation_id, payload_hash, state) VALUES (:tenant, :op, :hash, 'NEW') ON CONFLICT (tenant_id, operation_id) DO NOTHING; -- 2) Atomic claim: succeeds only if the hash matches and the state allows execution. UPDATE operation_ledger SET state = 'CLAIMED', lease_owner = :worker, lease_expires_at = now() + interval '5 minutes', attempt = attempt + 1 WHERE tenant_id = :tenant AND operation_id = :op AND payload_hash = :hash AND state = 'NEW' RETURNING attempt; -- 0 rows returned: SELECT the row and branch. -- hash differs -> QUARANTINED (changed-payload key reuse) -- SUCCEEDED -> safe duplicate; acknowledge -- REJECTED_TERMINAL -> acknowledge; outcome already recorded -- CLAIMED, lease live -> do not acknowledge; another worker owns it -- UNKNOWN -> route to reconcile, never to blind retryNotice that the claim only succeeds from NEW. A separate sweep moves expired CLAIMED leases to UNKNOWN, because a worker that vanished mid-flight may or may not have produced its side effect. That is the point where reconciliation, not re-execution, takes over.
Handler Contracts: Verified Acknowledgment, Terminal Failure, and Unknown Outcomes
Key takeaway: Acknowledge only after the outcome is durably recorded and any follow-up work is durably scheduled. Give every message exactly one of a small set of dispositions, including an explicit UNKNOWN.
A consumer that acknowledges first and records later loses work on a crash. A consumer that records and acknowledges separately without a recovery path leaves a window where the record exists but the broker will redeliver, or the message is gone but the record does not exist. The fix is to make deduplication, outcome recording, and work scheduling happen in one local transaction, then acknowledge. Our articles on the durable webhook receiver and the outbox pattern cover the mechanics.
Disposition table
| Disposition | Meaning | Acknowledge broker message? | Required before acknowledging |
|---|---|---|---|
| SUCCEEDED | Authoritative system confirms the effect | Yes | Ledger row updated with outcome reference |
| REJECTED_TERMINAL | Definitively invalid or not permitted; retrying cannot help | Yes | Reason code stored, owner notified or ticket created |
| RETRYABLE_FAILED (pre-effect) | Failed before any side effect, such as a validation call that never reached the target | No, or re-enqueue with delay | Evidence that no effect occurred |
| UNKNOWN | Completion could not be confirmed (timeout, lost connection, lease expiry) | Yes, but only after a reconcile task is durably scheduled in the same transaction | State = UNKNOWN plus scheduled reconciliation |
| STALE | Superseded by a newer operation on the same entity | Yes | Entity version comparison recorded |
| QUARANTINED | Needs human decision: unparseable, hash mismatch, tenant mismatch | Yes | Minimal metadata stored; payload access restricted |
Two definitions deserve emphasis. UNKNOWN means completion could not be confirmed. It is not success, and it is not verified failure. Dashboards that fold UNKNOWN into "failed" invite blind retries; dashboards that fold it into "succeeded" hide lost work. And uncertain input (a message missing a tenant ID, with an unrecognized schema version, or whose tenant does not match the queue's expected tenant context) must never be guessed at. Quarantine it.
Illustrative handler flow
# Illustrative pseudocode. Names are placeholders, not a specific SDK. def handle(msg): env = parse_envelope(msg) # tenant_id, operation_id, schema_version, payload if env is None or not tenant_context_matches(env, msg): record_quarantine(msg.metadata_only(), reason='UNPARSEABLE_OR_TENANT_MISMATCH') return ACK # durable record exists, so acknowledging is safe h = sha256(canonical_json(env.payload)) claim = ledger.try_claim(env.tenant_id, env.operation_id, h, worker_id) if claim.kind == 'HASH_MISMATCH': record_quarantine(env.ids_only(), reason='CHANGED_PAYLOAD_KEY_REUSE') return ACK if claim.kind in ('SUCCEEDED', 'REJECTED_TERMINAL', 'STALE'): return ACK if claim.kind == 'LEASED_ELSEWHERE': return NO_ACK # let the broker redeliver after the visibility window if claim.kind == 'UNKNOWN': schedule_reconcile(env.tenant_id, env.operation_id) return ACK # only because the reconcile task is durable # claim.kind == 'CLAIMED' try: result = target.apply( idempotency_key=env.tenant_id + ':' + env.operation_id, expected_version=env.payload.expected_version, # target-side conditional mutation body=env.payload) except Timeout: # In ONE transaction: state=UNKNOWN and enqueue reconcile task ledger.mark_unknown_and_schedule_reconcile(env) return ACK except VersionConflict: ledger.mark(env, 'STALE') return ACK except PermanentRejection as e: ledger.mark(env, 'REJECTED_TERMINAL', reason=e.code) return ACK ledger.mark(env, 'SUCCEEDED', outcome_ref=result.id) return ACKIf you build a custom redriver for selective replay (because native redrive cannot filter), apply the same discipline: republish to the source queue with the original operation ID and payload, confirm the publish, record the redrive decision, and only then remove the DLQ copy. A crash between publish and delete can create a duplicate, which is exactly why the consumer's ledger must tolerate it.
Service Bus settlement caveat
Microsoft documents that in peek-lock mode, closing the receiver or connection before settling leaves the message locked until the lock expires, after which it is redelivered with a higher delivery count. Settle each message before closing the receiver, and if settlement fails because the lock was lost, receive and process the message again rather than retrying settlement on the stale copy. With the ledger above, that second processing pass resolves to a safe duplicate instead of a repeated side effect.
Privacy, Retention, and Ownership of Dead-Letter Data
Key takeaway: A DLQ is a data store. Give it an owner, an access model, a retention decision, and a place in erasure workflows, and avoid copying payloads into tickets, chats, or logs.
Dead-lettered messages usually hold full payloads, which may contain personal data, commercial terms, or credentials that should never have been in a message. Even "just identifiers" can be sensitive: tenant IDs, user IDs, email addresses, and account numbers can identify people or reveal business relationships. That has three practical consequences.
- Triage on metadata, not payloads. Dashboards and alerts should carry operation ID, tenant reference, reason code, and timestamps. Payload access should be a separate, logged, least-privilege action.
- Retention is a trade-off you must own. Amazon SQS guidance recommends a DLQ retention period longer than the source queue's, so messages are not deleted before you can investigate. That pulls against data minimization. Pick a retention period tied to your realistic investigation and reconciliation window, document it, and alarm on age.
- Erasure scope must include DLQs and triage copies. If your deletion workflow covers databases and search indexes but not dead-letter queues, quarantine stores, or replay archives, it is incomplete. See our verified erasure workflow design for how to track deletion across systems.
Which privacy or contractual obligations apply to your queues depends on your jurisdiction, customers, and agreements. Provider features such as access policies and encryption options are compliance-support tools, not proof that your system meets any particular obligation; confirm scope in the provider's primary documentation and with your own counsel and security team.
Access control worth setting up
SQS lets you attach a redrive allow policy to a dead-letter queue that controls which source queues may use it (all, specific queues by ARN up to 10, or none), and requires the DLQ to be in the same account and Region as the source. Using these controls, one DLQ per workload or tenant tier, rather than one shared DLQ for everything, keeps blast radius small. The trade-off is more queues to monitor.
Ownership matrix
| Decision | Accountable owner | Evidence to keep |
|---|---|---|
| DLQ depth and age alarms | On-call SRE or DevOps lead | Alarm config, runbook link |
| Classification of failure cause | Service owner (consumer team) | Triage record with reason codes |
| Replay approval for consequential operations | Service owner plus domain or finance owner when money or customer data is affected | Signed-off go/no-go checklist |
| Payload access | Security or data owner | Access log, break-glass reason |
| Retention and erasure scope | Data protection owner or equivalent | Documented retention period, erasure workflow coverage |
Design trade-offs at a glance
| Choice | Benefit | Cost or risk |
|---|---|---|
| Native redrive | Fast, no code, rate-limitable | No filtering, no tenant isolation, new message ID, ordering interleaves |
| Custom selective redriver | Per-tenant, per-cause control; fits ledger | Code to build, test, and secure; must itself be idempotent |
| Long DLQ retention | More time to investigate | More stored personal data; larger erasure scope |
| One DLQ per workload | Smaller blast radius, clear ownership | More alarms and operational surface |
| Strict hash binding | Prevents silent content drift | Legitimate corrections need a new operation ID and a recorded approval |
Why This Fails in the Real World: Common Failure Patterns
Key takeaway: Capable teams still fail at redrive because the incident pressure rewards draining the queue, and the safety properties live in places nobody checks. The scenarios below are composite and illustrative, not reports of specific customers.
Failure pattern 1: The recovery stampede
A downstream service had a partial outage. Thousands of messages dead-lettered. The team shipped a fix, saw the dependency recover, and started a redrive in the default, fastest mode. The replayed traffic plus live traffic exceeded what the dependency could absorb, so the dependency degraded again, and the same messages started dead-lettering a second time. Nobody was careless: the console offered one obvious button, and the runbook did not require a rate.
Illustrative arithmetic: suppose a DLQ holds 120,000 messages and your downstream has measured headroom of 40 additional requests per second beyond live traffic. A throttled replay takes about 3,000 seconds, or 50 minutes. An unthrottled replay at the documented SQS ceiling of 500 messages per second would offer 12.5 times the safe rate and finish its transport in about 4 minutes, which is a speed the downstream cannot honor. These numbers are hypothetical; measure your own headroom.
| Replay rate (illustrative) | Time to drain 120,000 messages | Relation to 40/s headroom |
|---|---|---|
| 10 msg/s (canary) | about 3.3 hours | 25% of headroom |
| 40 msg/s | about 50 minutes | 100% of headroom, no margin |
| 30 msg/s (with margin) | about 67 minutes | 75% of headroom |
| 500 msg/s | about 4 minutes | 12.5x headroom |
Failure pattern 2: The duplicate that looked like a new message
A consumer deduplicated on the broker message ID. That worked in normal operation, where redelivery keeps the ID. After a redrive, every message had a new ID and a new enqueue time, so the consumer treated each as new work. Orders that had actually completed (the handler succeeded but the acknowledgment was lost) were executed again. Intelligent teams miss this because the dedupe passed every normal-path test, and the redrive path was never rehearsed. The control is the business-level operation ID in the ledger, plus a replay rehearsal in a non-production environment.
Failure pattern 3: The silent expiry
A standard SQS queue had a long retention period. The DLQ was created later with a shorter default. Because standard-queue messages keep their original enqueue timestamp when dead-lettered, some messages that had waited in the source queue were deleted from the DLQ sooner than anyone expected. Depth alarms stayed quiet because the queue looked small. The control is to set DLQ retention longer than the source, alarm on oldest-message age, and review retention whenever either queue's configuration changes.
Failure pattern 4: The ambiguous timeout replayed as a failure
A payment-adjacent handler timed out calling a provider, marked the message failed, and eventually dead-lettered it. During recovery, the team replayed it. The original call had in fact succeeded. Without an UNKNOWN state and reconciliation, the team's data said "failed" when the truth was "completion unconfirmed." The control: classify timeouts as UNKNOWN, reconcile against the provider's authoritative record, and use a provider-honored idempotency key where one exists.
Early warning signals
- DLQ depth rising while source-queue throughput stays flat (consumers are rejecting, not stalling).
- Oldest-message age in the DLQ approaching its retention period.
- A single exception signature accounting for most dead-letters (likely a code defect, a good canary candidate after the fix).
- Handler duration percentiles approaching the lock or visibility timeout.
- Hash-mismatch or tenant-mismatch quarantines above zero (a producer bug or a security signal; investigate, do not suppress).
Is your dead-letter queue a backlog or a liability?
If replay safety depends on one engineer remembering the right console setting, the design is not finished.
Talk to Developers.dev engineers about queue recovery design, observability, and DevOps operations for your stack.
Contact UsConclusion: Decide Before You Replay
Redriving a dead-letter queue is a decision about consequential work, not a queue-hygiene task. These are five actions to take this quarter:
- Add a business operation ID and payload hash to every queued message type, and stop using broker message IDs for deduplication. Back them with a tenant-scoped ledger and a unique constraint.
- Introduce explicit dispositions, including UNKNOWN and QUARANTINED, and make "acknowledge" conditional on a durable outcome record plus any scheduled follow-up work.
- Write a replay runbook built on all-required go/no-go controls, with a mandatory rate cap, a canary slice, and a tested pause path.
- Rehearse a redrive in a non-production environment that mirrors your broker settings, and verify that duplicates, stale operations, and changed payloads are handled correctly.
- Assign owners for DLQ access, retention, and erasure scope, and add DLQs and triage stores to your deletion workflow.
Remember the scope of this guidance: the lifecycle and controls above are proposed designs, the code is illustrative rather than an official SDK contract, and provider limits should be re-verified against current documentation. Your legal and contractual obligations are specific to your organization.
About the Developers.dev team
Developers.dev is an offshore software development and staff augmentation company founded in 2007, with 1000+ in-house IT professionals delivering for clients in the USA, EMEA, and Australia. The company states that it holds CMMI Level 5, SOC 2, and ISO 27001 accreditations. Those statements describe Developers.dev's own delivery processes; they do not make any client system compliant. Our engineers work on cloud, DevOps, and site reliability engineering engagements; see our DevOps services or learn how to hire DevOps engineers for your team.
Reviewed by the Developers.dev Expert Team.
Frequently Asked Questions
What is the difference between a dead-letter queue and a retry queue?
A retry queue holds messages scheduled for another attempt, usually with a delay. A dead-letter queue holds messages the broker stopped delivering after a limit (such as a maximum receive count or delivery count) or because they could not be processed. A retry queue implies "try again"; a DLQ implies "a decision is needed."
- Retry queues are typically automated and time-bounded.
- DLQs need classification, an owner, and a documented release process.
Is it safe to use native DLQ redrive for everything?
Only when every consumer is idempotent on a durable business identity and the downstream can absorb the replay rate. Native redrive in Amazon SQS, per the vendor documentation, cannot filter or modify messages and assigns new message IDs and enqueue times to redriven messages. It is a transport feature. Use it with a rate cap and a canary batch, and use a custom redriver when you need per-tenant or per-cause selectivity.
Why can't I use the broker message ID as my idempotency key?
Because redrive can change it. SQS documents that redriven messages are treated as new messages with a new message ID. Use an operation ID created where the business intent originates, scoped by tenant, and bind it to a hash of the approved payload.
What should I do with a message whose handler timed out?
Do not assume it failed. Record the outcome as UNKNOWN, schedule reconciliation against the authoritative system, and decide on replay only after you know what happened. For targets that support idempotency keys or conditional writes, use them so a later retry cannot duplicate the effect.
How long should dead-letter data be retained?
Long enough to investigate and reconcile, and no longer. Vendor guidance, such as SQS's recommendation to keep DLQ retention longer than the source queue's, protects against premature deletion, but longer retention increases stored personal data and erasure scope. Set the period deliberately, alarm on message age, and have the data owner approve it.
Should FIFO queues use a dead-letter queue?
It depends on whether strict ordering matters more than isolating poison messages. The SQS documentation advises against using a DLQ with a FIFO queue if you do not want to break the exact order of messages or operations, and redriven messages interweave with new producer traffic. If ordering is a correctness requirement, enforce it with per-entity version checks in the handler or target system rather than relying on queue order alone.
How do I replay messages for only one tenant?
Native SQS redrive does not filter, so build a selective redriver that reads from the DLQ, checks tenant and operation state in the ledger, republishes eligible messages to the source queue with their original operation IDs, and removes the DLQ copy only after confirming the publish. Because a crash can still produce a duplicate, the consumer's idempotency ledger must handle it.
Need a second set of engineering eyes on your message-recovery design?
Idempotency gaps, unthrottled replays, and unowned dead-letter data tend to surface at the worst possible moment.
Developers.dev DevOps and site reliability engineers can review your queue consumers, observability, and replay runbooks with you.
Contact UsProject Rescue Services
This guide is designed for engineering leaders who want to plan a practical implementation. Use the related Developers.dev path to compare delivery options, implementation fit, risk, and practical next steps.
Reviewed by the Experts team
This guide is reviewed for clarity, technical and operational relevance, service alignment, and a useful next step. Verified by our SEO team for clear search presentation.
Reviewed by the Experts team. Verified by our SEO team. Validate legal, security, data, budget, and operational requirements with the relevant stakeholders before rollout.

