Research & Analysis · Software engineering · 17 min read
How Do You Retry an API Request Without Duplicating the Operation?
Retry an uncertain API operation only when you can preserve its original identity and satisfy the destination's documented retry contract. That means checking the operation, parameters, account and regional scope, retained idempotency key, and permitted retry window. If those conditions cannot be established, reconcile the original outcome before starting another operation. A timeout tells you that the caller did not obtain a timely answer; it does not establish that the server did nothing.
The decision is more demanding than choosing a delay between requests. An integration must distinguish one business intention from several transmission attempts, and distinguish a returned response from the eventual business result. I would examine those distinctions before increasing a retry count. Otherwise, a change intended to improve availability can create duplicate work while making the dashboard look healthier.
This paper develops a proposed caller-side recovery method using twelve primary or authoritative works. It is technical synthesis, not a benchmark, systematic review or report of a production experiment. Provider examples retain their documented scope; none establishes a universal idempotency window. The worked case is fictional. The central research question is which evidence justifies retrying an outbound operation whose outcome is unknown, and when that uncertainty requires a different recovery path.
Treat a timeout as an observation boundary
Consider a request to create a production job. The caller sends it, waits, and reaches its timeout. Several histories fit that observation: the request never arrived; it arrived but was rejected; it is still running; or it completed and its response was lost. The application cannot choose among those histories merely by inspecting its own elapsed timer. Treating all of them as failure collapses a knowledge problem into an execution decision.
That distinction matters because the next request may be a new instruction. If the first job already exists, sending a second create operation can produce another job. If the first is still running, a retry might race with it. Cancelling the local wait also does not demonstrate cancellation at the destination. Remote cancellation, where supported, has its own contract and may itself need confirmation before a replacement is safe.
Saltzer, Reed, and Clark (1984) explain why application knowledge remains necessary for duplicate suppression: an apparent remote failure may be a delayed acknowledgement, and a new application attempt can duplicate earlier work even when lower network layers behave correctly. I apply that argument narrowly here. Reliable transport and connection recovery do not determine whether two business commands represent the same intention.
The useful first state is therefore outcome unknown. It should be visible in the operational record and recoverable after a process restart. It is neither proof of success nor permission to recreate the operation. A queue entry that records only “failed” discards precisely the distinction the recovery worker needs.
Separate business intent, attempts and observations
I would give each accepted business intention a durable local identity before attempting the external write. That record should connect the authorized operation, its stable parameters, destination scope and provider idempotency key. Each transmission then receives its own attempt record. A third set of observations captures provider request identifiers, resource identifiers, responses and later reconciliation evidence.
These are different cardinalities. One intention may produce several attempts but should have only the intended business effect. One attempt may generate multiple observations as an asynchronous operation progresses. Counting successful HTTP responses therefore cannot, by itself, establish either the number of business intentions fulfilled or the absence of duplicates. Metrics should preserve the mapping rather than flatten these entities into one request counter.
The local intention record must survive the failures the retry mechanism is supposed to tolerate. Generating a fresh key whenever a worker restarts defeats that property. Persisting the key only after receiving a response also leaves a gap: the external effect could occur before the caller saves the information needed to recognize it. Persisting intent before transmission addresses that client-side gap; it does not create an atomic transaction with a remote provider.
A local submission record also needs an explicit dispatch rule. If the application commits the intention but crashes before sending, a recovery worker must be able to discover it. If it sends before committing, recovery may have no durable identity to reuse. One proposed design is to commit the intention and its pending dispatch together in the local transaction, then let a worker send from that durable record. This is a design option, not evidence that the remote operation participates in the local transaction.
After sending, the worker still faces the uncertain interval between remote execution and recording confirmation. A local transaction can make local bookkeeping consistent; only the destination's supported duplicate detection or suitable conditional operation addresses repeat transmission across that interval. Testing both crash positions distinguishes an integration that loses unsent work from one that recreates already submitted work. The two defects require different repairs even though both may initially appear as an unfinished queue item.
An explicit caller identifier also expresses meaning that a payload hash cannot always infer. Featonby (n.d.) distinguishes repeated identical parameters from repeated intent and discusses storing the identifier with the original parameters. Two legitimate orders can contain identical items. My proposed rule is to create a new identity for a genuinely new authorized intention, and retain the original identity while recovering that intention. Similar payloads alone do not settle the decision.
Read the operation contract, not just the HTTP verb
RFC 9110 defines idempotency in terms of the intended server effect of repeated identical requests. It does not require identical responses or prohibit incidental per-request logging. The specification also advises against automatically retrying a non-idempotent method unless the client knows its semantics are idempotent or can determine that the original was not applied (Fielding et al., 2022, §9.2.2).
This provides a starting classification, not complete integration evidence. A documented POST operation may implement idempotency through an explicit key. A request carrying an invented header has no such protection unless the destination recognizes it. Likewise, an API's advertised method cannot excuse an implementation that attaches an unintended repeated business action to it. Review the operation actually invoked and the contract that governs it.
For each mutating operation, record what the provider treats as a duplicate, where that identity is scoped, which parameter changes it rejects, when it stores a result, how long it recognizes the key, and what a repeated response means. Include concurrent attempts and uncertain server errors. A statement that an SDK “supports retries” leaves these questions unanswered unless its configured behavior is connected to the specific operation.
I would also record which evidence is unavailable. A provider may offer neither a stable resource lookup nor a supported duplicate-detection mechanism. Client code cannot manufacture either guarantee. The design then needs a bounded manual resolution path, a different integration contract, or explicit acceptance of a particular duplicate risk. That limitation belongs in the business decision before an outage forces the choice.
Distinguish a repeatable error from a failed operation
Stripe's API v1 illustrates why status alone is insufficient. Its idempotency documentation says that, after endpoint execution begins, the first status and body are retained for a key, including a 500 response. Parameter-validation failures and conflicts with an executing request do not initiate endpoint execution and do not save an idempotent result. These are API v1 rules; the same page explicitly describes different API v2 behavior (Stripe, n.d.-b).
The separate error-handling guidance treats a 500 outcome as indeterminate and recommends resolving it through object inspection, request records and webhook evidence before trying a new key. Repeating the original key can return the cached error while the underlying outcome remains unresolved (Stripe, n.d.-a). This is an example of response stability without business-outcome certainty.
A recovery policy should consequently ask two questions: is another attempt permitted under this contract, and will another attempt improve knowledge or progress? A retained response may answer the first without satisfying the second. Once the response is known to be a replay of the same uncertain result, an endless request loop adds little information. Change the recovery mode to reconciliation while preserving the original operation identity.
Do not repair an inconvenient error by silently changing the parameters under the same key, or by issuing a fresh key until a success appears. A parameter correction can represent a different command. Establish the disposition of the earlier command first, then record the relationship between the rejected or completed intention and any authorized replacement. The application needs a defensible history, not merely a final green response.
Keep the key inside its documented scope
An idempotency identifier is not automatically global. EC2 documents regional and zonal idempotency, including cases in which the same client token can be used in a different Region or Availability Zone. RunInstances uses different scopes depending on how the Availability Zone is selected (Amazon Web Services, n.d.). A token copied into a failover request can therefore identify a distinct permissible operation in the new scope.
Adyen similarly documents company-account key scope and says duplicate checking does not extend across its regional endpoints. Its guidance also explains that possession of previously used keys can allow past-response retrieval despite reduced credential access (Adyen, n.d.). These are concrete reasons to review both recovery routing and handling of the identifiers. A key should not contain personal data or be treated as a harmless correlation string suitable for unrestricted disclosure.
My proposed local record binds the key to provider, environment, account, operation, effective API version and applicable region or zone. Some providers may not use all those fields in their deduplication lookup; retaining them still helps the caller detect an accidental contract change. A failover that changes any relevant scope should trigger a new decision rather than automatically inheriting the previous retry permission.
Authorization is another independent check. A known operation identity does not establish that the current caller may initiate additional work or retrieve its result. Reconcile permitted access with the provider's actual response-replay behavior and the application's current authority model. The related analysis of API authorization across untrusted clients develops that access question; this paper's narrower concern is preserving operation identity during recovery.
Make the retention window part of the recovery deadline
A finite deduplication window creates a boundary after which a familiar key may no longer identify remembered work. For Stripe API v1, the documented network-error guidance uses the first 24 hours for retrying the same key and parameters, followed by reconciliation if no outcome is known. Its key documentation permits pruning once keys are at least 24 hours old (Stripe, n.d.-a, n.d.-b). Do not turn that minimum retention statement into an unlimited promise.
The caller should record when its original request was first attempted, the documented window and any uncertainty in that timing. Prefer a conservative recovery cutoff when the provider's effective start time cannot be observed. Queue age matters: a worker recovering from a long outage must evaluate the existing intention's age, rather than start a fresh timer when it dequeues the record.
There are at least two different deadlines. The business may no longer want a new job after a dispatch cutoff, while the provider may still recognize retries. Conversely, the business may still want the job after the provider has forgotten the key. Neither deadline automatically resolves the original outcome. Stopping creation attempts should leave reconciliation active when an earlier effect could still matter.
I would make expiry a state transition with an owner and next evidence action. An expired intention should not disappear from a failed-work queue and return as a fresh order through a generic replay button. If the business authorizes a replacement despite uncertainty, record that exception and how a late original result will be handled. Calling the replacement a retry would conceal a changed risk decision.
Use preconditions without erasing the conflict they detect
Not all safe repetition depends on a response cache. Cloud Storage documents conditional idempotency for specified operations when they include generation, metageneration or ETag conditions. For example, its operation table associates object insertion and several related writes with an ifGenerationMatch precondition (Google Cloud, n.d.). The condition is part of what makes that operation suitable for automatic retry; it is not optional decoration.
The analytical distinction is between repeating a named intention and attempting a change against a named resource state. A precondition can prevent a second write from applying to a state that has already changed. It need not provide the first attempt's successful response, nor explain which actor changed that state. The caller still needs to determine whether its own earlier attempt succeeded or another operation intervened.
Preserve the condition when retrying the same attempted change. Automatically removing it after a conflict expands the command from “apply against this expected state” to “apply despite the changed state.” Automatically replacing it with the latest version can also authorize a different change. Either response may be appropriate in a new business decision, but neither is a neutral repair of the original attempt.
Reconciliation should inspect the resulting object and the available version or application correlation evidence. Mere existence may be insufficient when another writer could have created a similar object. Define what would identify this intention's effect, and acknowledge when the provider exposes too little information to distinguish the histories. Conditional writes control some races; they do not make all uncertainty observable.
Bound retry load separately from duplicate effects
A request can be safe from duplicate business effects and still be harmful to repeat aggressively. Brooker (2019) describes how retries during overload can worsen the overload, and why bounded backoff and jitter matter. Idempotency protects a semantic property. It does not make authentication, lookup, logging or response handling free.
Inventory retry behavior in the application, SDK, gateway, worker and queue delivery policy. Microsoft recommends placing retry logic where the failing operation's context is understood and warns that layered policies can add substantial delays (Microsoft, n.d.). A sensible budget belongs to the business operation across these layers, not merely to each local function call.
For a simple illustrative configuration, suppose an outer worker permits two total attempts and its SDK permits four transmissions per attempt. The downstream ceiling is eight transmissions, not four, before considering any additional delivery layer. Those numbers are hypothetical and include the initial attempt at each layer. Explicitly distinguishing attempts from retries prevents configuration documents from hiding another off-by-one multiplier.
Budget both elapsed time and transmissions. Backoff must fit the remaining business deadline and provider contract; jitter spreads scheduled work but cannot repair a scope error or expired key. When the budget is exhausted, retain the unresolved intention for reconciliation. Do not reset its budget merely by moving it to another queue. Track unresolved age and confirmed duplicate effects alongside retry volume so a quiet queue cannot disguise abandoned obligations.
Interpret retry signals within the protocol that defines them
A provider-specific signal can narrow the retry decision. Adyen documents a transient-error header with value true as permission to retry later using the same idempotency key; it also distinguishes concurrent requests and temporary unavailability of its idempotency resource. Its resilience discussion includes an option to omit the idempotency header and fall back to non-idempotent processing (Adyen, n.d.). That fallback explicitly changes the protection and needs a separate business decision.
Protocol-level retry signals can have different meanings. RFC 8470 defines 425 Too Early for a server unwilling to risk processing replayable early data. A retry following that response must not itself be sent in early data (Thomson et al., 2018, §5.2). Treating 425 as an ordinary delay instruction while preserving the same early-data behavior misses the condition the response is addressing.
I would express these rules as operation-specific policy entries rather than one status-to-delay table shared by every destination. Each entry should state the failure interpretation, unchanged fields, allowed transport change, stop condition and evidence source. A server error from one provider does not inherit another provider's execution guarantee. Nor does a successful TLS connection prove that an earlier application operation was absent.
This separation also makes maintenance practical. When a provider changes its API version or documented retry semantics, the team can identify the affected policy and evidence. A global helper that catches all errors and resends everything hides those differences in code that appears conveniently reusable but lacks the information needed to choose safely.
Reconcile the business result, not the response text
PayPal's documentation provides a useful contrast with cached-response behavior: where its request-ID mechanism is supported, a repeat returns the previous request's latest status, not necessarily its original status. Support and storage duration depend on the particular API, and simultaneous requests using the same ID can yield different outcomes at the request level (PayPal, 2026). Consequently, comparing response bodies for equality is not a general test of duplicate prevention.
For caller-side reconciliation, define the evidence that identifies the operation, the evidence that describes its current state, and the evidence needed to declare the intended result complete. A provider resource ID can identify an accepted asynchronous job without proving that the job finished. A webhook may supply later evidence, but its association with the intention and its trustworthiness must be checked before it changes the local record.
I would maintain distinct states for accepted locally, submitted with unknown outcome, confirmed provider operation, completed business result, confirmed non-execution, and unresolved exception. These are proposed semantic categories, not a prescribed database schema. A particular integration can combine states only if it preserves the decisions they represent. For example, “confirmed provider operation” should stop creation retries even while completion polling continues.
Reconciliation itself can race with retries. If a late success observation arrives while another worker prepares to submit, both must refer to the same durable intention. Coordinate the local decision so a worker does not create a new identity after confirmation. Where multiple independent systems can originate the business command, their ownership boundary also requires review; a local database lock cannot coordinate an actor that bypasses it.
Work the decision through a lost-response scenario
Consider the fictional Oriole Printworks, which submits production jobs to a fictional supplier. Assume, for this example only, a supplier contract that recognizes a key within one account and region for ten minutes and exposes a lookup by that key. These are invented contract terms used to examine the method, not claims about a named provider or measured operating results.
Oriole accepts one print order and durably records its intention, parameters, account, region and key before sending. The supplier creates the job, but the response is lost. The caller records an unknown outcome. At the second attempt, it retains the same key and parameters within the stipulated window. The supplier returns the existing job identifier. There have been two transmissions and one job under the assumed contract.
Now change one detail: the worker restarts and generates a fresh key from an in-memory retry helper. The second request can create a second job. Both HTTP responses and both keys may be individually valid; the defect is that one local intention has acquired two provider identities. The appropriate test therefore examines intention-to-job cardinality after restart, not just whether the retry helper returned successfully.
Change another detail: the worker resumes after twelve minutes. Its local record still contains the key, but the assumed guarantee has expired. It should use the stipulated lookup and resolve the original job before considering replacement. If the lookup is also unavailable, the correct record remains uncertain. An operator may decide that delayed fulfillment is preferable to duplicate printing, or authorize a bounded exception, but the system must not mislabel either choice as guaranteed safe repetition.
Test the uncertainty paths and acknowledge the limits
An acceptance exercise should inject failure at different boundaries: before transmission, after remote acceptance but before the caller receives a response, during concurrent same-intention attempts, and after local restart. Include changed parameters, expired retention, regional failover, late success observations and exhausted retry budgets. These are proposed tests; none was executed for this paper. Use isolated resources and an agreed supplier test environment where external effects are possible.
The strongest observation is the resulting business-effect count and its association with each authorized intention. Supplement that with recorded scope, key continuity, attempt history, provider identifiers and reconciliation transitions. A mock that always returns the same canned body can test local control flow but cannot establish a provider's production deduplication or failover behavior. Report that boundary rather than treating every green test as equivalent evidence.
There are reasonable objections to the proposed method. Durable intent storage and reconciliation introduce complexity, while harmless repeated reads may not warrant such machinery. Some low-consequence writes may tolerate duplicate cleanup. The response should be proportional to the effect, not a requirement to build a workflow engine around every request. Nevertheless, deliberately accepting duplication requires understanding it; a retry setting alone does not document that acceptance.
This synthesis also cannot guarantee eventual completion. A provider may remain unavailable, lose the evidence needed for lookup, or implement a contract incorrectly. At-most-once behavior and eventual success are separate properties. A workflow can avoid duplicates and still leave an order unfulfilled. The defensible objective is to preserve the distinction, bound automatic actions and assign unresolved obligations, rather than promise exactly-once business execution across systems this review cannot control.
Turn one integration into a reviewable recovery contract
The immediate deliverable I would seek is a recovery contract for one consequential operation: its durable intention identity, provider-specific retry conditions, scope and expiry, combined attempt budget, reconciliation evidence and exception authority. That makes the decision inspectable by engineering, operations and the business owner. It also identifies which uncertainties require better provider evidence rather than another retry loop.
For the wider design, security architecture review across trust boundaries provides the context for evaluating authority and external dependencies. Within this narrower problem, the decisive question remains concrete: after an unanswered request, what evidence permits another attempt without treating the same business intention as new work?
If an integration can issue orders, provision resources or trigger another consequential action, an integration reliability review can examine that question against a sanitized operation contract, retry configuration and example failure trace. The useful outcome is an explicit retry-or-reconcile decision and a prioritized set of missing safeguards. It is not a promise that a larger retry count will make an uncertain operation safe.
References
- Adyen. (n.d.). API idempotency. Retrieved October 29, 2026, from https://docs.adyen.com/development-resources/api-idempotency
- Amazon Web Services. (n.d.). Ensuring idempotency in Amazon EC2 API requests. Retrieved October 29, 2026, from https://docs.aws.amazon.com/ec2/latest/devguide/ec2-api-idempotency.html
- Brooker, M. (2019, December 5). Timeouts, retries, and backoff with jitter. AWS Builder Center. https://builder.aws.com/content/3EumjoZascWd1oZiEgL8ORlv3qE/timeouts-retries-and-backoff-with-jitter
- Featonby, M. (n.d.). Making retries safe with idempotent APIs. Amazon Builders' Library. Retrieved October 29, 2026, from https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/
- Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022, June). HTTP semantics (RFC 9110). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc9110
- Google Cloud. (n.d.). Retry strategy. Retrieved October 29, 2026, from https://docs.cloud.google.com/storage/docs/retry-strategy
- Microsoft. (n.d.). Retry pattern. Azure Architecture Center. Retrieved October 29, 2026, from https://learn.microsoft.com/en-us/azure/architecture/patterns/retry
- PayPal. (2026, August 11). Idempotency. https://developer.paypal.com/api/rest/reference/idempotency/
- Saltzer, J. H., Reed, D. P., & Clark, D. D. (1984). End-to-end arguments in system design. ACM Transactions on Computer Systems, 2(4), 277–288. https://web.mit.edu/saltzer/www/publications/endtoend/endtoend.pdf
- Stripe. (n.d.-a). Advanced error handling. Retrieved October 29, 2026, from https://docs.stripe.com/error-low-level
- Stripe. (n.d.-b). Idempotent requests. Retrieved October 29, 2026, from https://docs.stripe.com/api/idempotent_requests
- Thomson, M., Nottingham, M., & Tarreau, W. (2018, September). Using early data in HTTP (RFC 8470). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc8470