Eazler AI Lab AI Engineering Journal
Skip to article
Engineering◷ 16 min read

Agent retries and idempotency — a timeout is not proof of failure

A timeout is not proof of failure. What idempotency keys and checkpoints do and do not guarantee when an agent retries a call that changes external state.

On this page

The external service finished the operation, but the agent never received the response. If the agent now sends a new request, the same operation can run a second time. Stripe's own documentation tells integrators to treat the result of an HTTP 500 response as indeterminate. Designing retries starts with separating “operations that are safe to run again” from “operations whose completion must be checked first.” [1]

Korean original: 한국어 원문 (2026-09-10)Scope: tool calls that change external stateOfficial docs checked: 2026-09-10

1. Executive summary

Unknown
An outcome state for external operations,
kept separate from success and failure
24 hours
Stripe keys can be deleted once
they are at least this old [2]
27 calls
Example upper bound on bottom-layer calls:
three layers × 3 total attempts each
The key conclusion: When you retry the same external operation, keep its operation ID and inputs unchanged. If you could not confirm the outcome, resend the request only within the conditions and time window in which the external service blocks duplicate execution. Outside that window, hand the operation to a procedure: look up the result, reconcile the records, or have the person responsible confirm it.

Idempotency is the property that applying the same operation several times has the same intended effect as applying it once. An idempotency key is an identifier attached to a request so that the server can recognize a resend of the same operation. Attaching a key is not enough on its own. The receiving server has to process the key, and the key's retention period and scope have to fit your case. [2]

This article is a design interpretation of official documentation. The figure of 27 is not a production measurement; it is a calculation that assumes every layer uses up its attempt limit. The exact conditions are in section 6.

2. The response is not the outcome

Suppose an agent calls an order-placement tool. If the connection drops right after the server saves the order, all the agent sees is a timeout. The table below is not a record of a specific incident. It is a hypothetical scenario that illustrates a lost response.

MomentExternal order serviceWhat the agent knows
Before the requestNo order yetHas saved the order to place and its operation ID
ProcessingSaves the order and sends a success responseWaits for the response
Connection dropsThe order already existsGot no response, so does not know whether the order went through
RecoveryEither recognizes the same operation or creates a new orderDepending on how it resends, a duplicate can result

Stripe's documentation on network errors addresses this exact situation. It explains that the client may not know whether the server received the request, and that it should retry with the same idempotency key and the same parameters to get a definite result. For HTTP 500, it also advises against retrying with a new key, because external side effects may already have happened. [1]

The design proposal we draw from this is to split the outcome into separate states. Success means completion has been confirmed at the external service. Confirmed failure means you have evidence that the request was rejected and the intended external operation did not run. Unknown means there is not enough evidence to decide between success and failure. Requests that are still in flight are tracked separately, as an execution state.

A timeout marks the limit of what you observed. The fact that the wait for a response ran out does not let you conclude that the remote operation was cancelled. If you record an unknown operation as a confirmed failure, recovery can easily create a new operation.

Agents that “retry with a different tool when an error occurs” have the same problem. If the first service's completion is still unknown and you run the same task on another service, the first service's idempotency key does not protect you at the second service. Before switching to a fallback path, you also need to confirm the outcome of the original operation.

3. Identifying the same operation

The number of tool calls is not the number of operations you actually want done. In this article, a piece of work the user wants completed, such as placing an order, is called a business operation. If a user asked for one order and connection errors caused it to be sent three times, there is one business operation and three send attempts. If you do not keep the operation ID separate from the attempt ID, it becomes hard to tell whether a request is a retry of the same operation or a new one.

IdentifierLifetimeOur design proposal
operation_idOne entire business operationSaved before execution. Kept across restarts, resends and operator recovery
provider_idempotency_keyAll requests for the same operation sent to the external serviceStore the key actually sent, together with the service, account, environment and endpoint
attempt_idOne send attemptGenerated fresh for every attempt. Keeps logs, latency and errors apart per attempt
provider_object_idThe object created at the external serviceSaved as soon as it is confirmed. Used for later lookups and reconciliation

These field names are internal application names proposed for illustration. They are not standard API field names provided by Stripe or LangGraph. We do not assume that the tool call ID in a model response is the same as the business operation ID.

Storing an internal operation ID does not by itself mean you sent the same key to the external service. Whether you derive the external key from the internal ID or issue it separately, save the value you will actually send before the first send, and reuse it unchanged on every retry. If one business operation consists of several different external operations, keep a separate key for each of those external operations.

Do not let the model regenerate the operation ID

It is better for the application to create the business operation when it accepts the user's request, and to save its ID together with the normalized input before any external call. Any path that accepts the same request again must also link to the existing operation. Generating a new UUID right before the call avoids string collisions, but each restart may then be treated as a different operation.

For example, the client can create a client_request_token once on the order confirmation screen and reuse it even after a lost response. The server puts a unique constraint on the combination of customer account, operation type and this token, and maps the combination to a single operation ID. Set the token's retention period with the time window for user resends and operator recovery in mind. Use a new token for a new order intent. This is an example implementation. Issuing an independent operation ID for each request as it arrives does not, on its own, merge concurrent duplicate requests.

Be careful, too, about using a hash of the input in place of an operation ID. A hash is a value computed from the input data for comparison. The same customer can deliberately order the same product again, so identical input does not always mean the same business operation. Decide whether it is the same operation by the operation ID, and check whether the input changed against the stored input or its hash. Include the customer account and operation type in the identity scope as well.

Requests that arrive at the same time must also map to one operation

If you run “look up the existing record → create it if missing” as two separate steps, two concurrent requests can both conclude that no record exists. Register operations with a database unique constraint that blocks duplicate rows for the same ID, or with a single atomic operation that creates the record only when none exists. Assign the executor in that same step, while checking the operation's current state. But when an executor stops and the work is handed to another executor, whether the first external call completed may still be unknown. Design the local lock and the external API's duplicate protection together.

Do not change the inputs of an operation. If the amount, target or quantity has changed, it is not a simple resend. First settle the outcome of the previous operation, confirm the intent behind the changed operation, and then register it as a new operation. This section is an application design recommendation, not a universal API contract.

4. What Stripe's guarantee covers

Stripe's idempotency documentation is more specific than “the same key means it's fine.” The table below reflects Stripe's official documentation as checked on 2026-09-10. These rules do not carry over unchanged to other APIs. [2]

ConditionWhat the docs guarantee or limitWhat it means for your design
Requests that accept a keyAll POST requests accept idempotency keysCheck whether the external service and the specific endpoint support keys
Requests whose execution has startedThe status code and body of the first result are saved — 500s included, not only successesA resend with the same key does not necessarily turn into a success
Same key, different inputCompared with the original parameters; an error if they differUse the stored values so that the model cannot change the input during retries
Key retentionKeys at least 24 hours old can be deleted. Reusing a key after deletion creates a new requestFor long-pending operations, do not resend just because you still have the same key string
Validation failure or conflict with a concurrent requestEndpoint execution never started, so no idempotent result is savedDistinguish a replay of a saved result from a rejection that happened before execution

Do not read the 24 hours as permanent, business-level duplicate protection. Nor does it mean “keys are always deleted at exactly 24 hours.” If you cannot confirm whether a key older than that is still retained, it is safer not to resend on the assumption that the guarantee still holds. If an operator might reprocess an operation days later, use your local business records and lookups of the external object together. A resend after the key has been deleted may not return the earlier result. [2]

Stripe's advanced error handling documentation treats HTTP 500 results as indeterminate. It also describes putting a local identifier in the metadata so that objects created later can be matched up through webhooks. Reconciliation is this procedure: checking local operation records against the actual results at the external service. [1]

So whoever handles recovery has to ask not only “should I resend with the same key?” but also “does an object already exist?”, “are we still inside the key's protection window?” and “was the response replayed while the external state changed later?” Even if a lookup returns nothing, when that API's lookups lag behind its writes you cannot immediately treat the empty result as evidence that nothing ran.

5. Execution left outside the checkpoint

A checkpoint is workflow state saved so that execution can resume later. LangGraph saves state at step boundaries (it calls these steps “super-steps”) and says that even if another node in the same step fails, the saved results of the nodes that succeeded can be used for recovery. A node here is a unit of execution inside the workflow. [3]

But there is a gap between success at the external service and the local save of that result. If a node places an order and stops before saving the result, the saved graph state alone does not let you conclude that no order exists. This is a design inference that combines the checkpoint description with the lost-response scenario in section 2. It is not a duplicate rate measured in LangGraph itself.

MechanismProblem it addressesBoundary to check separately
CheckpointRecovery from saved execution stateExternal operations that finished before their result was saved
External API idempotency keyKeeping the effect of the same request from being duplicatedKey scope, matching input, retention period
Transactional outboxCommitting the DB change and the record of the event to send togetherEvent resends and duplicate handling at the consumer

Even with the outgoing event saved alongside the data, consumers must handle duplicates

A transactional outbox records the business data change and the event to be sent in the same database transaction, after which a separate relay sends the event. AWS describes it as the answer to the dual-write problem, where only one of the database write and the message send succeeds. It also states explicitly that events can be sent more than once, so consumers should track the messages they have processed and handle them idempotently. [5]

Applied to an agent's tool execution, this lets you review two things separately: “not losing work that should run” and “not duplicating external side effects.” Adding a job table or a queue does not by itself guarantee that an external side effect happens only once. Check the guarantees about message delivery and the guarantees about state changes at the external service, each on its own.

A starting point for small teams: Store the operation ID, inputs, result and external object ID in the persistent database you already use, and start by checking whether the API you will call supports idempotency. Consider an outbox when a business data change and the registration of an execution request must be committed together. Decide separately whether to adopt checkpoints, based on your workflow's recovery requirements.

6. Retry budgets and recovery procedures

Start by agreeing on what a retry count means. LangGraph's RetryPolicy.max_attempts is the total number of attempts, including the first, and its documented default is 3. That is the parameter's default when you use a retry policy; it does not mean retries are applied to every agent automatically. [4]

The following is our own worked example, not the default combination of any particular product. It assumes that tool retries by the agent, node retries in the workflow and resends by the HTTP client are stacked on top of each other.

Assumed layerTotal attempts per call from the layer aboveUpper bound on calls accumulated down to the bottom layer
Agent tool retries3 attempts3 calls
Workflow node retries3 attempts3 × 3 = 9 calls
HTTP client resends3 attempts3 × 3 × 3 = 27 calls

This is the upper bound when every layer treats the error as retryable and uses up its limit each time, with no success, cancellation or expiry of an overall deadline. If your “3 retries” setting means 3 additional attempts after the first call, that is 4 total attempts per layer, so putting 3 into the same calculation would be wrong.

Backoff means waiting longer before each next attempt as failures repeat, and jitter means adding random variation to the wait. LangGraph provides both settings, but tuning the wait and preventing duplicate execution are separate problems. [4]

Decide who owns retries and when everything stops

Our recommendation is to manage the total attempt count and an overall deadline per business operation in the application, and to include the automatic retries of lower-level SDKs in that budget. Once the allowed attempts are used up, check the operation's state so that the model cannot register the same business operation again as a new one. Give reads and requests that change external state separate retry policies, each suited to its nature.

Confirmed stateNext stepWhat to avoid
External success confirmedSave the result and external object ID, and return the existing resultCreating a new business operation because the local response was lost
Rejection, with non-execution confirmedFix the cause; if needed, approve and register a new operationResending the same bad input indefinitely
Network error with no response received, duplicate-protection conditions metA limited resend with the same key and input, or a result lookupChanging the key, or switching straight to another service
An HTTP 500 result saved by StripeConfirm the actual outcome through webhooks, object lookups and reconciliation against external recordsReading a replay of the same 500 as settling success or failure, or running the operation with a new key
Outcome unknown, with key retention unclear, protection expired or keys unsupportedHand off to reconciliation against external records or to a person's reviewRunning it again without checking and closing it as a success

If the external service does not guarantee idempotency and offers no lookup that can reliably confirm completion, it is hard for automatic recovery alone to both prevent duplicates and guarantee that the operation completes. In that case, a clear stopping procedure is to record the original request, the last attempt and whatever could not be confirmed, so that a person can decide.

7. Checklist to apply today

  • Pick one tool that changes external state. Start with operations whose effects add up when repeated, such as placing an order, publishing a post or creating an account.
  • Save the business operation ID before execution. Check that restarts and user resends link to the same operation.
  • Freeze the execution inputs. Make sure the model does not regenerate the target, amount or content during retries.
  • Write down the external API's contract. Record whether keys are supported, their scope, the retention period, and how concurrent calls and error responses are handled.
  • Create an “unknown” outcome state. Decide where an operation goes once the retry budget is exhausted: a lookup, reconciliation or confirmation by the person responsible.
  • Count the total calls. Add up the attempts of the model, the workflow and the SDK, and set a limit per operation.

Instead of the happy path, break it at these four points

Run this in a test environment, not on real customer operations, and observe the external results independently. The tests below are what this article proposes. They are not measurements we ran for this report.

Fault or condition to injectPass criterion
Response lost after external successRecovers with the same key; no additional external object is created
Crash after external success, before the local result is savedAfter restart, looks up and recovers the same business operation without creating a duplicate
Two requests for the same business operation arrive at onceBoth map to one business operation, and only one external side effect occurs
An unknown operation whose external key can no longer be assumed to be retainedRecords whether retention is unclear or protection has expired, and moves to the reconciliation path instead of resending automatically

Looking only at the retry success rate can hide duplicate operations. It is better to also record the number of external objects relative to the number of business operations, how long unknown operations wait, and the actual number of sends per operation. Log collection itself is covered in the agent observability conventions report.

8. What this is not yet

  • We did not measure production duplicate rates. This article is a design guide that interprets official product documentation and distributed-systems patterns. It gives no figures for cost savings or failure reduction from the proposed structure.
  • Stripe's 24-hour condition is not a rule for every API. Check other services' key retention periods and scope separately, in the documentation for the endpoint in question.
  • Online documentation alone cannot verify how the LangGraph version you have installed behaves. We checked the online docs on 2026-09-10; compare the retry settings against your installed version. How checkpoints are stored and your persistence settings also affect what can be recovered.
  • The figure of 27 assumes every layer uses up its attempt limit. Real call counts vary with error classification, deadlines, cancellation, success and SDK policy.
  • Idempotency does not prevent a wrong first execution. Running once against the wrong target is a problem for permissions, input validation and approval steps. Security controls are covered separately in the coding agent security report.
  • Idempotency keys and a job table alone cannot guarantee that every external side effect happens only once. There will still be cases where automatic recovery has to stop, depending on whether the external service supports keys, whether its lookups are conclusive, late responses and retention policies.

9. Sources

All are primary sources checked on 2026-09-10. Product documentation describes the vendor's own behavior; it is not independent experimentation. Operation states, field names and test setups that do not appear in these sources are marked in the body as our design proposals.

SourceWhat we checkedScope of the evidence
[1] Stripe · Advanced error handlingRetrying network errors, the indeterminate result of a 500, reconciliation through webhooks and metadataStripe API's error-handling contract
[2] Stripe · Idempotent requestsSaving the first result, comparing inputs, keys deletable once at least 24 hours old, rejection before executionStripe's description of its idempotency implementation
[3] LangGraph · CheckpointersCheckpoints at step boundaries, saving and recovering the results of successful nodesFramework state persistence. Not a transactional guarantee that covers external APIs
[4] LangGraph · Fault toleranceRetryPolicy's total attempt count, backoff and jitter parametersPolicy definitions in the current docs. Not measured in an installed environment
[5] AWS · Transactional outbox patternRecording business data and events together, duplicate events and consumer idempotencyA distributed-systems design pattern. Not an agent-specific performance study

Editorial standard: we did not treat a timeout as proof that the remote operation failed, and we kept the guarantees the products document separate from the recovery procedures the application has to add.

Newsletter

The week’s new reports, every Sunday

One email a week with that week’s reports, skipped when nothing new came out. Nothing but report news.

Used only to announce new reports. Unsubscribe any time. Privacy Policy
Publisher

Eazler AI Lab is the AI engineering journal of Eazler Inc. We read primary sources in full, and say so when something is unverified.

Editorial standards and method →

We also post report summaries on Threads: @eazler.inc.

Building or adopting AI agents? hello@eazler.com