Your real problem isn’t setting retries it’s knowing what business state you’re in after a failure and taking the smallest safe step.
Design cross-system delivery around at-least-once behavior, duplicate safety, and recovery visibility not fragile exactly-once promises. Exactly-once across organizational and network boundaries is a bet you can’t reliably win. The reality is: a timeout is an unknown outcome. That’s where integration failure recovery patterns earn their keep.
Different failures carry different risks, so choose the smallest safe action:
- Bounded retry: Handle transient uncertainty with capped attempts and backoff (your integration retry strategy).
- Idempotent handling: Make duplicates safe with keys or safe upserts (sound idempotent API design).
- DLQ isolation: Quarantine messages that keep failing and control when/how they’re replayed (dead-letter queue handling).
- Reconciliation: Compare systems when state may be unknown or divergent, then correct.
- Compensation: Offset completed business steps when you can’t reverse them directly (the compensating transaction pattern).
Below is a failure-to-recovery matrix that starts with what you can prove after a failure and maps it to a safe next step. We’ll back it with two operations assets: a production runbook for on-call use, and a pre-release testing checklist to prove your guardrails ownership, evidence collection, replay limits, and a clear business-state closure condition before you ship.
Start With Integration Failure Recovery Patterns, Not Exactly-Once Promises
When independent systems talk over unreliable networks, the safest baseline is at-least-once delivery. Design for duplicates, not against them. That means explicit duplicate detection, idempotent handling where possible, and reconciliation when state might have drifted. Integration failure recovery patterns exist because uncertainty is normal; pretending we can guarantee exactly-once delivery across boundaries only hides risk.
HTTP verbs don’t save you. A PUT or DELETE suggests intent, but HTTP method semantics alone don’t create business-level idempotency for externally visible changes. Two identical POSTs might create two invoices. A PUT that “should” overwrite might still trigger a duplicate shipment. Business idempotency lives in your domain rules and storage, not in the verb.
And a timeout proves nothing. It doesn’t prove success or failure; it proves you don’t know. After a timeout, your integration retry strategy must assume ambiguity: safely retry with idempotent API design, look up definitive status if available, or queue the item for later reconciliation. Treating timeouts as failures (or successes) without evidence is how double charges, duplicate accounts, and orphaned records happen.
Before you act, decide with evidence. Use these criteria to pick a safe recovery path:
- Classification: Is this a transient, persistent, or business rule fault?
- Duplicate risk: Could a retry cause a visible duplicate or side effect?
- Committed work: Did any downstream step already complete?
- Status availability: Can you query an authoritative status by id/correlation key?
- Reversibility: Can you undo effects with a compensating transaction pattern?
- Ownership: Who operates the retries, DLQ, and reconciliation, and with what SLAs?
Also, be clear about what tools don’t guarantee. A queue, an idempotency key, an outbox, or an HTTP method each helps, none ensures exactly-once business effects. Queues can deliver twice. Keys expire or get misapplied. Outboxes replicate messages faithfully, including bad ones. Dead-letter queue handling isolates poison messages but doesn’t fix them; you still need controlled replay and guardrails.
The reality is, reliability comes from choosing the smallest safe recovery action for what you can prove right now. In the next section, we’ll match each failure mode to the right action—bounded retry, DLQ isolation and replay, reconciliation, or compensation so you can close business state confidently without overpromising.
Match the Failure Mode to the Safest Recovery Action
Use the matrix below to pick the smallest safe move for each failure mode. For broader planning context on where to start, see our related perspective in SaaS API integration strategy.
| Failure Mode | What Is Known | Safest First Action | Closure Condition |
|---|---|---|---|
| Timeout | No timely response; downstream processing unknown. | Check status via key/correlation ID. If unavailable, use bounded retries with an idempotency key or queue for reconciliation. | Single confirmed outcome recorded and linked to key. |
| Dependency Outage | Dependency unavailable (DNS, connect errors, 5xx spikes). | Open circuit breaker, fail fast, defer to queue/outbox. Recover via controlled replay and rate limits. | Backlog drained to steady state, circuit closed, SLOs stable. |
| Partial Commit | Some steps completed; others failed or remain unconfirmed. | Read state before writing. Resume missing steps or reconcile. Compensate confirmed steps if workflow cannot complete. | Systems reach consistent intended state or compensation completes. |
| Duplicate Request or Event | Same request or event arrived again. | Enforce idempotency key lookup, short-circuit processing, return stored outcome. | Single record/effect persisted; duplicate logged as no-op. |
| Poison Message | Message repeatedly fails due to data or code defect. | Isolate to DLQ. Fix underlying defect and replay with guardrails or execute compensation/reconciliation. | Message reprocessed or formally rejected with owner sign-off. |
Timeout
What we know: We didn’t get a timely response; the downstream may have processed the request or not.
Primary risk: Duplicate side effects if the original succeeded and we retry blindly; lost business event if we don’t.
Safest first action: Prefer a status check (lookup by idempotency key or correlation ID). If unavailable, use a bounded integration retry strategy with exponential backoff and an idempotency key. If uncertainty persists, queue for reconciliation rather than looping.
Required evidence: Correlation or idempotency key, request/response logs, any provider status/read API (for example: GET /operations/{key} → {status: PENDING|SUCCEEDED|FAILED, resourceId}).
Closure condition: Single confirmed outcome recorded (success with one effect or explicit cancellation) and linked to the idempotency key.
Dependency Outage
What we know: The dependency is unavailable (DNS/connect errors, 5xx spikes) and is unlikely to process requests right now.
Primary risk: Resource exhaustion and cascading failures from indefinite sync retries; thundering herd on recovery.
Safest first action: Open the circuit, fail fast, and defer to a durable queue/outbox. Use bounded retry with backoff only for short, known transient windows. Recover with controlled replay: rate-limit, warm-up checks, and progressive ramp.
Required evidence: Health checks, error codes by class, circuit metrics, queue depth.
Closure condition: Backlog drained to steady-state without error spikes; circuit closed and SLOs stable.
Partial Commit
What we know: Some business steps completed; others didn’t (e.g., order created, payment uncertain).
Primary risk: Divergent state or double-processing if we repeat unsafe steps.
Safest first action: Read-before-write using idempotent API design (keys or natural business keys). Resume only missing steps. If state can’t be proven, move to reconciliation before any replay. Use the compensating transaction pattern only to offset steps that are already confirmed.
Required evidence: External status by key, audit/event logs, outbox/message journal showing what was emitted and acknowledged.
Closure condition: Systems show a consistent, intended state or a documented compensation is complete and auditable.
Duplicate Request or Event
What we know: The same request/message arrived again (replay, client retry, at-least-once delivery).
Primary risk: Duplicate business effects (double charge, second shipment).
Safest first action: Enforce idempotent handling: detect by idempotency key or deterministic key, short-circuit side effects, and return the stored outcome. Add reconciliation only if prior effects are unclear.
Required evidence: Idempotency store hit, dedupe index, or response cache tied to the key.
Closure condition: Exactly one durable record and effect for the key; duplicates logged as no-ops.
Poison Message
What we know: The same message fails after retries (schema/validation bug, code path, non-recoverable data).
Primary risk: Hot loop, consumer starvation, and partial side effects on each attempt.
Safest first action: Isolate via dead-letter queue handling. Diagnose and fix. Approve controlled replay with guardrails (versioned handlers, canary replay, rate limits). If any step already committed, apply compensation; otherwise reconcile business state after replay or permanent drop.
Required evidence: Retry count, stack traces, payload snapshot, root-cause notes, change approval for replay/compensation.
Closure condition: Message successfully reprocessed or formally rejected with documented disposition; backlog normal and business owner sign-off recorded.
Use Bounded Retries for Transient Failures and Dependency Outages
You don’t retry everything. You retry only what’s likely to succeed on a second attempt and won’t create a bigger mess if it does. Classify the error first, then pick the smallest safe move. This keeps recovery efforts focused on outcomes rather than knobs.
- Retriable transients: Think short-lived network blips, throttling, or a dependency that’s briefly overloaded. Bounded retry is appropriate.
- Deterministic failures: Validation errors, missing required data, permission issues, and other “it will never work” problems. Don’t retry; surface the error for correction or route to a non-retry path.
- Unknown outcomes: The call may have been processed but you didn’t get the response (for example, a timeout). Do a status check first. If you can’t prove state, prefer idempotent API design and reconciliation over blind retries.
- Repeatedly failing payloads: Stop. Isolate to a dead-letter queue handling path for diagnosis and controlled replay, not endless retries.
A safe integration retry strategy is bounded and measurable. Write it down, enforce it in code, and instrument it.
- Finite attempts and a hard deadline: Limit retries by count and by end-to-end elapsed time. Whichever limit hits first stops the loop and triggers defer-or-fail handling.
- Capped exponential backoff with jitter: Increase wait times between attempts up to a cap. Add randomness to prevent thundering herds when many workers retry at once.
- Per-dependency retry budgets: Track and cap total in-flight retries per downstream system. When a budget is exhausted, shed load (fail fast) or defer work to a durable queue/outbox.
- Outcome-aware guards: For operations with duplicate risk, require idempotency keys and a status/read-before-write check on retry. That’s how retries stay safe without leaning on a compensating transaction pattern later.
Define how you behave during sustained failure with a circuit breaker. When error rates breach a threshold, open the circuit: stop synchronous retry loops, fail fast to callers, or defer work to a queue. Use controlled probes (small, timed test requests) to detect recovery and only then close the circuit to resume normal traffic. If a payment gateway is down, open the circuit, defer charges to your outbox, and probe its health endpoint before sending real volume again. Before deferring or resending any charge, verify whether the earlier attempt succeeded or explicitly reuse its original idempotency key to avoid double-charging.
Configuration must be dependency-specific. Copy-paste defaults don’t survive production. Validate retry counts, backoff caps, deadlines, and budgets with load tests and failure injection until you see stable success rates and acceptable latency. For concrete production techniques that pair well with these policies, see our guidance on scalable APIs in production.
Require telemetry so you can prove the policy works and spot regressions:
- Attempt count and elapsed time per request
- Failure cause and categorized error type (transient, deterministic, unknown)
- Per-dependency failure and retry rates
- Path taken: immediate success, bounded retry success, fail-fast, deferred, or DLQ/replay
Next, we’ll make those retries and replays safe at the business layer with idempotency.
Make Partial Commits and Duplicate Requests Safe With Idempotency
Retries and timeouts keep your system moving, but they also create duplicate requests and “did it commit?” uncertainty. That’s where idempotent API design turns risk into a controlled, observable workflow. If you’re modernizing an existing API surface, our overview of APIs and application modernization outlines how to evolve contracts without breaking consumers.
Define a clear idempotency contract for any externally visible mutation:
- Caller-supplied key: The client provides a unique idempotency key per intent (e.g., “charge order 123 once”). The server enforces uniqueness within the caller or tenant scope. Use high-entropy identifiers (such as ULIDs or UUIDv7s) to ensure uniqueness across distributed systems.
- Caller or tenant scope: Bind the key to the authenticated principal or tenant so keys can’t collide across customers. The server must verify that a presented key belongs to the caller’s tenant.
- Request fingerprint: Persist a hash of the meaningful payload fields to detect “same key, different body.” Choose a stable canonicalization so semantically identical payloads hash the same.
- Durable state: Store the key, fingerprint, request time, status (PENDING | IN_PROGRESS | SUCCEEDED | FAILED), and any resource identifiers in durable storage.
- Recoverable prior result: Persist the canonical response (or a pointer to it) so you can safely return the same outcome to duplicates.
Make the API behavior explicit and testable:
- Key expiry: Set a TTL long enough to cover your worst-case retry window, DLQ replays, and human-triggered replays (often days to weeks). After expiry, check the underlying target resource or business record before treating the request as a new intent to ensure no duplicate operations occur.
- Same-key payload mismatch: If a duplicate arrives with a different fingerprint, reject deterministically (409 Conflict) and return the original request details to help callers reconcile.
- Concurrent duplicates: When the same key arrives in parallel, the first creates the mutation; others should not race. Respond with a pending status (202 + operation URL) or block briefly on the in-flight operation document which.
- Completed duplicates: If the first attempt finished, return the stored result with a stable idempotency marker. Don’t re-run side effects.
- Status lookup: Provide GET /operations/{idempotencyKey} so clients can check progress instead of spamming retries. Scope this endpoint to the authenticated tenant to avoid cross-tenant information leaks.
- Collision handling: If a presented key accidentally matches an existing key from another intent within the same tenant (extremely rare with good entropy), require the caller to use a new key; do not overwrite the existing record.
HTTP verb choice isn’t enough. POST vs. PUT doesn’t protect business state from retries, reordering's, or transport duplicates. Idempotency lives at the business level: one customer charge, one shipment, one entitlement grant.
Handle partial commits by making the idempotency record and the business mutation atomic or equivalent:
- Single transaction: Use a single database transaction that writes the idempotency row and applies the change (or reserves a unique business identifier).
- Reconcilable pending: If a timeout leaves completion unknown, keep the key in PENDING and reconcile: read the authoritative system using the key or business identifier, then mark SUCCEEDED and return the stored result, or continue the operation safely. Don’t guess.
Close the update-to-publish gap with a transactional outbox. Write the business update and an outbox message in the same transaction; a relay delivers the message later. But relays can duplicate publication, so consumers must also deduplicate using a stable event ID or business key and a processed-keys store. This keeps your recovery patterns consistent end to end: safe retries, safe replays, and no double effects. When repeated failures persist, you’ll isolate them with dead-letter queue handling in the next section. If you want implementation help building durable integration controls, our integration and migration services can support the architecture and rollout.
Handle Poison Messages With DLQ Isolation, Controlled Replay, and Reconciliation
When a message keeps failing after bounded retries, stop letting it cycle through normal processing. That’s where a dead-letter queue (DLQ) helps. Treat the DLQ as an isolation point for repeatedly failing or deterministic errors never as an automatic fix. It buys time to investigate without blocking healthy traffic or compounding damage. These safeguards are a core part of operational resilience in production.
A DLQ only works if you keep the evidence with the work. Every dead-lettered item should carry:
- Correlation and trace: Correlation IDs and trace context (end-to-end and per hop)
- Failure classification: Transient, data defect, code defect, contract mismatch, unauthorized
- Diagnostic artifacts: Retained request/response snippets or hashes, last stack/error, and timestamps
- Named ownership: A named owner or team on call for the queue
- Retention planning: Ensure evidence and recoverable work don’t expire before you can act
Use a simple, auditable replay sequence. Don’t skip steps, even under pressure:
- Inspect and classify: Determine why it failed and whether it’s safe to retry.
- Correct first: Fix the defect, data condition, or contract on at least one side.
- Verify idempotency: Confirm downstream idempotent API design and dedup protections are in place.
- Assess ordering impact: Evaluate FIFO/causal ordering if this message is injected late or across groups.
- Scope the replay: Select the exact messages (IDs, time window, tenant) and define success/failure criteria.
- Approve: Require human approval in change control for the scoped replay plan.
- Rate-limit: Drip-feed replays and set backpressure limits; pause on rising error rates.
- Observe: Track per-message status, lag, and error classes in real time.
- Reconcile outcomes: Confirm business-state closure conditions; escalate to reconciliation or a compensating transaction pattern if mismatches remain.
SQS example (clearly labeled):
- Redrive policy: max Receive Count defines how many receives occur before a message moves to the DLQ. Set bounded retries in line with your strategy; beyond that, isolate.
- Retention: SQS message retention defaults to 4 days and can be configured up to 14 days. Ensure the DLQ retention matches or exceeds the source queue’s retention to give operators enough time to analyze and replay failed messages before expiration.
- FIFO impact: Moving a message to a DLQ can alter FIFO behavior. A stuck message can block its group; redriving later may interleave with newer events. Validate ordering needs and idempotency before replaying.
Platform-neutral guidance: If you’re not on SQS, you still need equivalent controls clear isolation, message inspection, approval workflow, rate control and cancellation, replay observability, and reconciliation. Don’t just copy SQS settings; implement the capabilities that let your team prove safety.
The reality is, poison messages are a signal to slow down, gather evidence, and repair safely. Use DLQ isolation to protect the system, then apply controlled replay with guardrails. If the business state still diverges after replay, move to reconciliation or compensation in the next step.
Reconcile or Compensate When Business State Still Needs Repair
If bounded retries, idempotent handlers, or a controlled replay still don’t close the business state, choose between reconciliation and the compensating transaction pattern. Pick the smallest safe action that brings the workflow to a clear, auditable end.
- Reconciliation (compare and correct): Reconciliation means comparing expected vs. actual business state after unknown outcomes, partial commits, deferred work, or replay then correcting or escalating mismatches. You’re answering, "What did we intend, what really happened across systems, and what must we fix?" Typical moves include reapplying a missing update, closing a dangling reservation, or raising a case when records disagree and you can’t safely infer the truth.
- Compensation (offset, not roll back): Compensation is an explicit, offsetting business action when an earlier local step completed but a later step can’t. It is not a guaranteed literal rollback. If payment was captured but shipment can’t be created, the compensating action is a refund and inventory release, not pretending the capture never happened.
A simple rule: reconcile when the outcome is unknown or divergent; compensate when you know a step succeeded and must be offset because the workflow can’t complete.
Guardrails for compensation
- Idempotent by design: Each compensating action must be idempotent. Use an idempotency key (the original workflow or order ID) so repeating the compensation doesn’t double-refund or double-release inventory. This is where solid idempotent API design pays off.
- Clear ownership: Name the owner that can authorize and execute each compensating action (domain team, finance ops, customer service). No owner, no compensation.
- Know the pivot point: Identify the “point of no return” in the workflow. Before the pivot, prefer retry or reconciliation. After the pivot, use compensation. Example: inventory reserved (pre-pivot), card captured (pivot), shipment failed (post-pivot) ⇒ release inventory and refund capture.
Closure conditions that actually close
Don’t declare victory because a queue is empty or a transport retried successfully. Close only when one of these is verified:
- Processed: The intended downstream effects exist and match the source of truth.
- Reconciled: Records across systems agree after compare-and-correct, with evidence stored.
- Compensated: All required compensations executed and confirmed idempotently.
Document the evidence (correlation IDs, timestamps, who approved), and attach it to the incident or runbook record so auditors and on-call engineers can see why the case is closed. For examples of how teams resolve ERP/CRM drift in the wild, see our discussion of ERP CRM integration data conflicts.
Notes on related patterns
- Transactional outbox helps, but isn’t enough: It closes the publication gap between your database commit and message publish, reducing “lost events.” It doesn’t replace reconciliation for unknown outcomes or compensation when a completed step must be offset.
- Where this fits among recovery patterns: Use a retry strategy first for transient uncertainty, apply dead-letter queue handling and controlled replay for repeat failures, then move to reconciliation or compensation to restore business-state correctness.
Hypothetical example: You sent “create invoice,” the ERP shows no invoice, but your payment gateway shows a settled charge. Reconcile by comparing ERP, payment, and your order ledger. If the invoice truly doesn’t exist, either recreate it (reconciliation) or, if billing is no longer valid, issue a refund (compensation). Only close once the records match or the refund is confirmed, not when the message simply leaves the queue.
Create a Production Recovery Runbook and a Pre-Release Test Checklist
You can’t improvise during an incident. Put a production runbook in the hands of the on-call, and keep a separate pre-release test checklist so practice never pollutes live steps. Keeping these two artifacts clear and current is itself one of your most important operational safeguards.
Production Recovery Runbook (who acts, what to record)
- Triage: Confirm blast radius, customer impact, and the workflow pivot already crossed. Decide if bounded retry is safe per your integration retry strategy.
- Containment: Pause consumers or routing to stop spread. Quarantine repeats via dead-letter queue handling where needed.
- Fault classification: Label as transient uncertainty, duplicate risk, repeated failure, unknown/divergent state, or completed-with-damage (compensating transaction pattern likely).
- Evidence capture: Before change, snapshot logs/metrics/traces and payloads.
- Owner assignment: Name the active owner in each domain:
- Service behavior & retry policy: Application team
- Queue/broker operations: Platform/SRE
- Business-data reconciliation or compensation authorization: Ops/finance/business
- Retry/replay approval: Require sign-off from service owner and broker owner. Verify idempotent API design and replay guardrails (keys/tokens) before any action.
- Reconciliation: If state is unknown or divergent, run compare-and-correct with the business owner’s sign-off.
- Compensation escalation: If work already crossed the pivot, escalate to the compensation approver and execute the compensating transaction pattern.
- Closure: Declare one of: processed, reconciled, or compensated. Document customer comms and preventive actions.
Required runbook fields to capture every time:
- Policy: Location or version of the recovery policy used.
- Correlation: Correlation/trace IDs; message IDs.
- Retry data: Attempt counts, elapsed time since first failure, backoff applied.
- Cause: Suspected/confirmed root cause and evidence.
- Queue state: Depth, DLQ counts, consumer lag, pausing status.
- Replay scope: Message range, filters, and guardrails used.
- Closure evidence: Screenshots/queries proving processed/reconciled/compensated.
Pre-Release Test Checklist (kept separate)
- Fault injection: Induce network slowness, drops, and server faults.
- Timeout ambiguity: Prove safe behavior when response is unknown; verify bounded retry rules.
- Duplicate delivery: Validate idempotent API design with keys/tokens on retry and replay.
- Partial commits: Simulate mid-workflow failures around the pivot.
- DLQ replay: Test dead-letter queue handling, scoped replays, and operator prompts.
- Rate limiting: Backoff, jitter, and circuit breaking under quota errors.
- Cancellation: Verify safe stop/cancel during in-flight retries or replays.
- Ordering impact: Check out-of-order messages and compensations.
- Reconciliation: Run compare-and-correct on realistic drift datasets.
- Compensation paths: Dry-run approvals and execution, ensure idempotency.
For additional implementation context as you plan and staff these practices, see our perspective on APIs as the backbone of modern app development.
Conclusion
Integration failure recovery patterns aren’t “done” when a retry works or a queue is empty. They’re done when you can prove business-state closure orders settled, balances correct, and no strays left to surprise you. The runbook and test checklist you just built make that repeatable: collect evidence, choose the smallest safe step, and record closure.
- Use bounded retry: Apply a disciplined integration retry strategy only for transient uncertainty, then stop and surface evidence.
- Design idempotent APIs: Make calls and messages tolerant of duplicates; idempotent API design is your default guardrail.
- Isolate to DLQ and replay under controls: Quarantine repeated failures with dead-letter queue handling and replay with auditability.
- Reconcile with source-of-truth: When state is unknown or divergent, confirm reality before touching production data.
- Apply compensating transactions: Use the compensating transaction pattern when business steps completed and must be offset once, with proofs.
Here’s the thing: recovery succeeds when it closes the loop on business state, not when it promises “exactly once.” If you want help pressure-testing your highest-risk flows and turning these choices into guardrails, Moltech Solutions Inc. offers System Integration Services to assess and strengthen them.










