Skip to content
AI.info

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

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

Google's SRE retry rule, per 1,000 requests: three attempts allow at most 3,000 attempts to arrive, while the 10 percent retry budget caps the same traffic at 1,100.

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.

FigureProcess · 5 steps
  1. 1

    Deadline

    A maximum wait for one attempt, chosen from task latency needs.

  2. 2

    Error classification

    Separates transport, validation, authorization, conflict, and business failures.

  3. 3

    Idempotency key

    Links repeated requests to one logical operation.

  4. 4

    Outcome lookup

    Queries whether the original action committed.

  5. 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.

FigureComparison · 3 columns

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.

FigureProcess · 5 steps
  1. 1

    Classify each tool

    Mark reads, idempotent writes, conditional writes, and non-idempotent effects.

  2. 2

    Map error classes

    Define retry, repair, poll, compensate, and terminal responses.

  3. 3

    Choose backoff

    Add delay and jitter without exceeding the task deadline.

  4. 4

    Preserve operation identity

    Reuse one logical key across every attempt.

  5. 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