AI agents
Timeouts, Retries, and Idempotent Tool Use
Handle ambiguous tool outcomes without duplicating side effects or hiding incomplete work.
By the end you can
- Define retry-safe tool execution as an operational contract rather than a capability label
- Contrast Safe read retry with Idempotent write in “A payroll agent paid a contractor twice after a network timeout”
- Trace “Retries can create a second incident after the first action succeeded” through a concrete execution path
- Produce “Write a retry matrix” with evidence for “Repeated execution produces at most one logical side effect”
A timeout proves nothing about whether the operation ran
A timeout means the caller lacks a timely result. It does not prove that the operation failed.
Retrying is safe in three cases only: the tool is idempotent, the original outcome can be queried, or the action carries a stable idempotency key. That test is not house style. It is written into the HTTP standard. RFC 9110 defines idempotency and names PUT, DELETE and the safe methods as the idempotent ones. Then it makes the rule normative: “A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent, regardless of the method, or some means to detect that the original request was never applied.”
The same passage closes the two obvious escapes. A proxy MUST NOT automatically retry non-idempotent requests. A client SHOULD NOT automatically retry a failed automatic retry. The entitlement to send a request twice comes from knowledge — of the semantics, or of what happened. It never comes from the mere fact that no answer arrived.
Retry policy should therefore depend on error class, side-effect semantics, and remaining task budget. Uniform retries turn transient uncertainty into duplicated work.
Uncertainty after a timeout has to be resolved by asking the system what happened, because no amount of reasoning about the error will recover the answer.
Case
Retry budgets at Google, idempotency keys at Stripe
Both halves of this problem have settled, published answers. Google's SRE book caps retries with a budget. A request fails after three attempts — “If a request has already failed three times, we let the failure bubble up to the caller” — and a client keeps retrying only while its retries stay under a tenth of its requests: “A request will only be retried as long as this ratio is below 10%”. Stacking the policy is ruled out in one line: “If multiple layers retried, we’d have a combinatorial explosion.”
Stripe answers the other half with a key. Its API reference says that “Stripe’s idempotency works by saving the resulting status code and body of the first request made for any given idempotency key … Subsequent requests with the same key return the same result”, and that “You can remove keys from the system automatically after they’re at least 24 hours old.”
What gets skipped is the edge the key does not cover. Stripe states it plainly: “We save results only after the execution of an endpoint begins. If incoming parameters fail validation, or the request conflicts with another request that’s executing concurrently, we don’t save the idempotent result because no API endpoint initiates the execution.” A key does not make every repeat a replay. A call that never started leaves nothing to replay. Two calls racing under one key are not one call.
The in-flight case has an answer of its own. The IETF draft that specifies the Idempotency-Key header says a retry arriving while the original is still being processed SHOULD receive HTTP 409 Conflict rather than a replayed result. It names Stripe and Adyen as implementers. So an idempotency key has three outcomes, not one: the saved result, the 409 while the first attempt is still running, and nothing saved because nothing began.
Figure
Position
A retry loop buys another attempt, not reliability
Success on the second attempt looks like a system that recovered. It can equally be a system that did the work twice. A timeout tells the caller it has no timely result. It does not tell the caller that the operation failed. That gap is the whole problem.
A duplicate request is not free, and the price has been measured rather than argued. Dean and Barroso published the measurement in 2013: “For example, in a Google benchmark that reads the values for 1,000 keys stored in a BigTable table distributed across 100 different servers, sending a hedging request after a 10ms delay reduces the 99.9th-percentile latency for retrieving all 1,000 values from 1,800ms to 74ms while sending just 2% more requests.” They also give the general rule for keeping that bill small. Defer the second request until the 95th-percentile expected latency, and the extra load stays at approximately 5%. Note what makes those numbers admissible. The duplicate is a read. The extra load is counted. The delay before the second attempt is chosen so the count stays in single digits.
Stripe's design shows what the write case costs instead. The status code and body of the first call are saved under the idempotency key. Later calls carrying that key receive the same result rather than a fresh effect. The key may be pruned once it is at least 24 hours old.
Retry counts are somebody's budget too. Google's SRE book lets a request fail after three attempts and holds a client's retries below ten percent of its requests. It warns that retrying at several layers at once produces a combinatorial explosion. Set a demonstration that always eventually succeeds against 2%, 5% and 10%. An unbounded loop over a tool that is not idempotent, cannot be asked what happened, and carries no stable key has not been made reliable. It has been given more chances to cause the same effect twice.
A tool that cannot be retried safely does not become safe because the runtime retries it.
Visual
How retry-safe tool execution moves through the runtime
A deadline expires, the error is classified, the idempotency key decides whether a second attempt is safe, and an outcome lookup settles what actually happened. Nobody should own both the outcome lookup and the retry budget. No single test should cover both.
The retry budget is the step that looks most like bookkeeping and is most often the cause of the outage. A 2022 study ranked what kept 22 metastable failures going, across incidents reported by 11 organisations, and retry policy came first: “By far, the most common sustaining effect is due to the retry policy, affecting more than 50% of the studied incidents”. Huang and colleagues name the affected cases individually: GGL2, GGL3, AWS1, AWS2, AWS3, AZR2, AZR4, IBM1, SPF1, SPF2 and CAS2. The same study attributes at least 4 of the 15 major AWS outages of the preceding decade to metastable failures, with outage durations running from 1.5 to 73.53 hours.
Read against the diagram, that finding relocates the danger. The trigger sits at the left of the path and is usually brief. What kept more than half of those systems down was the policy at the right-hand end, still running long after the thing it was compensating for had passed.
- 1
Deadline
A maximum wait for one attempt, chosen from task latency needs.
- 2
Error classification
Separates transport, validation, authorization, conflict, and business failures.
- 3
Idempotency key
Links repeated requests to one logical operation.
- 4
Outcome lookup
Queries whether the original action committed.
- 5
Retry budget
Limits attempts, delay, and cumulative cost.
Comparison
Tradeoffs that change retry-safe tool execution
Safe read retry, idempotent write, and non-idempotent write are three different contracts. Only the third can turn a timeout into a second charge. The first two are safe to resend: a read changes nothing, and an idempotent write collapses duplicate calls onto one key, so ten attempts leave behind exactly what a single attempt would have. RFC 9110 draws the same line by naming names — PUT, DELETE and the safe methods are the idempotent ones. That is why the argument always happens over a POST or a PATCH.
The third contract makes no such promise, so the decision to send it again cannot be taken inside the tool. It has to be taken by the runtime, before the duplicate is ever on the wire. Moving a method out of that third column is exactly what the Idempotency-Key header is for, and the IETF draft that specifies it says so: “The HTTP Idempotency-Key request header field can be used to make non-idempotent HTTP methods such as POST or PATCH fault-tolerant.” A non-idempotent write does not become an idempotent write because the runtime would like it to be one. It becomes one when a key, a saved result and a defined answer for the in-flight case — HTTP 409 Conflict — are all actually implemented behind it.
Safe read retry
A repeated read has no external side effect.
- Usually low risk
- Still affected by staleness
- Needs backoff
Idempotent write
Repeated requests converge on one logical effect.
- Supports recovery
- Requires stable keys
- Must define scope
Non-idempotent write
Every attempt may create another effect.
- Requires outcome lookup
- Often needs approval
- Blind retry is unsafe
Example
What a review of retry-safe tool execution should inspect
Everything else rests on one measurement: that repeated execution produces at most one logical side effect. Separate from it, and easy to conflate with it, is whether the runtime distinguishes unknown outcome from confirmed failure. A timeout says nothing about whether the write landed, and treating it as a failure is a guess. Two more checks only show themselves under repeated or adversarial cases: that retry counts and elapsed budget are visible in the trace, and that non-retryable errors do not enter model-driven retry loops.
The last of those looks like the least important signal on the list. It is not. Almost every catastrophic failure in a 2014 study of 198 randomly selected user-reported failures — from Cassandra, HBase, HDFS, Hadoop MapReduce and Redis — came from code that had already been told something was wrong. Yuan and colleagues put it in one line: “almost all (92%) of the catastrophic system failures are the result of incorrect handling of non-fatal errors explicitly signaled in software”. The systems had detected the problem and said so. In 58% of those cases the fault could have been caught by simple testing of the error-handling code. In 35% the error handler was empty or log-only, aborted the cluster on an over-general exception, or still contained “FIXME”/“TODO”. A retry loop driven by a model is another error handler nobody tested, sitting on the same path where 92% of the damage was decided.
- Signal 1: Repeated execution produces at most one logical side effect.
- Signal 2: The runtime distinguishes unknown outcome from confirmed failure — the case Stripe answers with a saved status code and body, and the in-flight case the IETF's Idempotency-Key draft answers with HTTP 409 Conflict.
- Signal 3: Retry counts and elapsed budget are visible in the trace, against stated limits: three attempts and 10% in Google's SRE book, 2% and about 5% extra load in the hedging rule of Dean and Barroso.
- Signal 4: Non-retryable errors do not enter model-driven retry loops — the error-handling path where Yuan and colleagues traced 92% of catastrophic failures, 58% of them reachable by simple testing.
Key idea
Retries can create a second incident after the first action succeeded
The agent may interpret silence as permission to try again. This is especially dangerous for payments, messages, provisioning, deletion, and physical control.
AWS published what that looks like at scale. On 7 December 2021 an automated scaling activity at 7:30 AM PST provoked a surge of connection activity. It congested the devices between the internal and main AWS networks. Then the loop closed on itself. AWS's own summary of the event puts it in one sentence: “These delays increased latency and errors for services communicating between these networks, resulting in even more connection attempts and retries.” A latent bug stopped the well-tested client back-off from working. Congestion improved only at 1:34 PM PST. All network devices recovered at 2:22 PM PST. The metastable-failures study classifies the same event independently as case AWS4, 12/07/21, lasting 9.3 hours with “retry” as the sustaining effect.
The payroll case has a real twin too, and it is on a regulator's record. On 1 August 2012 Knight Capital's order router, SMARS, was handed 212 small retail orders. The SEC's order records what happened next: “While processing 212 small retail orders that Knight had received from its customers, SMARS routed millions of orders into the market over a 45-minute period, and obtained over 4 million executions in 154 stocks for more than 397 million shares.” Dormant “Power Peg” code was doing the sending. Its cumulative-quantity counter told it to stop sending child orders once the parent order was filled. That counter had been moved elsewhere in 2005 and never retested. Knight lost over $460 million. The SEC censured it and imposed a $12,000,000 civil money penalty. The stop condition, not the sending, was the part that had to work.
Use stable operation identifiers, verify uncertain outcomes. Escalate when the tool cannot support safe retry semantics.
The second attempt costs nothing to make and everything to undo when the first one already went through.
Steps
Write a retry matrix
Write the retry matrix for one workflow: every tool down one axis, every error class along the other, and a decision in each cell. The cells you cannot fill in with confidence are the ones where retries can create a second incident after the first action succeeded. A timeout on a write, most often, where the caller cannot tell a lost response from a lost request. Beside each of those, note what the tool would have to offer — an idempotency key or a status lookup — before the cell has a safe answer to put in it.
Give each cell the numbers it has to live inside: the three attempts and the 10% ceiling from Google's SRE book if you are borrowing that policy, or the delay-then-hedge discipline that held Dean and Barroso's extra load to 2%. Then write the stop condition for every non-idempotent tool as its own line, and test it. Knight's router had one. It had been moved in 2005. Nothing on the way to 1 August 2012 asked it to prove it still fired.
- 1
Classify each tool
Mark reads, idempotent writes, conditional writes, and non-idempotent effects.
- 2
Map error classes
Define retry, repair, poll, compensate, and terminal responses.
- 3
Choose backoff
Add delay and jitter without exceeding the task deadline.
- 4
Preserve operation identity
Reuse one logical key across every attempt.
- 5
Test lost responses
Simulate success with a dropped reply and verify that no duplicate effect occurs.
Name what stops the second attempt, or do not retry
The agent should never invent retry semantics. They belong in the tool contract and runtime policy. That is where RFC 9110 puts them: a client SHOULD NOT automatically retry a non-idempotent method without some means to know the semantics are idempotent anyway, or some means to detect that the original request was never applied. A team revisiting retry-safe tool execution should start from the incidents the current policy makes possible, not from the wording of the policy, and only then adjust the budgets and backoff. AWS had a well-tested client back-off on 7 December 2021 and a latent bug that stopped it firing. The wording of that policy was never the problem.
The question to answer before release is narrow. For every call this policy will resend, name the mechanism that stops the second attempt from doing the work twice — the saved status code and body, the 409 on the in-flight case, the outcome lookup. Where no mechanism can be named, the call does not get resent.
A retry rule improvised inside the model is a rule nobody reviewed, nobody versioned, and nobody can switch off.
Key takeaways
- A timeout means the caller lacks a timely result; it does not prove that the operation failed — and RFC 9110 turns that into a normative SHOULD NOT on automatically retrying a non-idempotent method, plus a MUST NOT for proxies.
- An idempotency key has three outcomes, not one: Stripe saves the status code and body of the first request, saves nothing when validation fails or two calls race under the same key, and the IETF's Idempotency-Key draft answers the in-flight retry with HTTP 409 Conflict.
- A duplicate request can be priced. Dean and Barroso cut 99.9th-percentile latency from 1,800ms to 74ms across 1,000 keys on 100 BigTable servers for 2% more requests, and about 5% when the second request waits for the 95th-percentile expected latency.
- Retry policy is the most common sustaining effect in Huang and colleagues' 22 metastable failures across 11 organisations, present in more than 50% of them, with outages running from 1.5 to 73.53 hours.
- Error handling is where the damage is decided: 92% of the catastrophic failures in Yuan and colleagues' 198 cases came from mishandled non-fatal errors the software had explicitly signalled, 58% of them catchable by simple testing.
- Name the mechanism that stops the second attempt, or do not resend — AWS's 7 December 2021 US-EAST-1 event (9.3 hours, sustaining effect: retry) and Knight Capital's 45 minutes, 397 million shares and $460 million loss are what a missing stop condition costs.