| Takeaway | Detail |
|---|---|
| SDK defaults are too conservative for agent call failures | The 99.9% reality check from 'Building for Failure: Exponential Backoff Strategy' means designing for APIs that fail, not accepting a default retry budget that stops before the recoverable tail. |
| Backoff needs a durable fallback | PeekAPI flushes events in batches and applies exponential backoff on failure; because the 99.9% bar allows no silent drops, failed events are persisted to a JSONL file. |
| Retries should be visible in operations | ReTraced represents retries as observable data in a distributed job queue, matching the 99.9% reliability requirement that retry behavior be inspectable and auditable. |
| One more retry is worth it for agent workloads | Agent traces of alert-summarization tasks show recoverable failures cluster at the retry most default policies skip, so the 99.9% standard argues for exceeding SDK defaults. |
The centerpiece of the field guide 'Building for Failure: Exponential Backoff Strategy' is a 99.9% reality check: engineers must design for APIs that fail. That number exposes a quiet mismatch in agent development. OpenAI, Anthropic, and LangChain ship default retry policies that are too small for production LLM calls, and the retry they skip is where a meaningful slice of recoverable failures actually die.
A Stanford agent trace makes the point concrete. An alert-summarization workload sat at the edge of failure until one more backoff cycle ran; when that extra retry executed, the stuck tasks completed cleanly, and no further retry was needed. The failure pattern was not server failure or bad input—it was simply retry arithmetic.
The fix is not endless retries. It is exponential backoff with a durable escape hatch. Middleware such as PeekAPI buffers events, retries with backoff, and persists failures to disk instead of dropping them; ReTraced turns retries into observable data. Together they meet the 99.9% bar: the failure path is part of the design, not an afterthought.

The Retry Math
Hitesh Singh Solanki's 2026-01-03 worker-service hardening post frames exponential backoff as the "Retry ..." approach instead of just logging an error when a task fails — but the difference between recovery and a self-inflicted outage is not the decision to retry; it is the exact math of the schedule. In an agent step, a failure is any non-success result from an LLM, a tool, or a vector store, and the first action is classification, before any timer starts. Only certain transient server errors and gateway timeouts qualify as retryable. Client errors are permanent: the request is malformed, unauthenticated, or semantically rejected, and retrying one of those just burns a slot in the three-retry cap. Exponential View's 2026 piece on AI adopters notes that "success and failure look identical — at first"; classification is the step that tells them apart.
The schedule is fixed: after the initial failure, wait, then wait longer, then wait longer still. The total scheduled backoff across all three waits is the sum of those waits; if the third retry succeeds, only the first two waits were actually consumed. The third retry is not a formality. In 2026, LLM throttling is bursty, not binary; the rate-limit window typically closes between the second and third retry, making the third attempt the first one that can plausibly return a success response. Stopping before the final retry is the policy that lets a share of failures become a permanent outage; the third attempt is where the first success appears.
Each wait uses full jitter: the sleep is drawn randomly between zero and an exponentially growing bound for each retry. The cap never binds in this schedule — it exists to bound naive doubling schemes that grow past it. The point of jitter is synchronization. Salars.net's 2026-04-19 failure-scenarios report opens with "Exponential systems fail exponentially": without jitter, concurrent agents all retry at the same correlated boundaries, and the backoff becomes a thundering herd in disguise.
Before computing any of that, read the Retry-After header. OpenAI's and Anthropic's rate-limited responses can return Retry-After; when present, use that value instead of the calculated exponential delay, but keep the same three-retry cap. The server's explicit wait replaces the local estimate, not the budget.
The backoff count is necessary but not sufficient. Attach an idempotency key to every retryable call, following Stripe's Idempotency-Key pattern; without it, retrying a POST tool call can execute a payment twice even when the backoff count is correct. Yehezkiel Dio Sinolungan's 2026 Medium report documents a bot that can run FFmpeg ten times in parallel for lack of a deduplication cache. And when the cap is exhausted, terminate: PeekAPI's resilient design, documented on Hacker News, persists failed events to a JSONL file on disk instead of dropping them. The cap is what makes the policy finite; the fallback is what makes it honest.
| Status class | Responses | Verdict | Why |
|---|---|---|---|
| Transient | Transient server errors and gateway timeouts | Retry with escalating full-jitter delays | Rate-limit window closes between retry 2 and retry 3 |
| Permanent | Client errors (malformed, unauthenticated, semantically rejected) | Never retry | Malformed or unauthenticated; retry burns a slot in the three-retry cap |
| Success | Success responses | Stop | Terminal state; no backoff consumed |

What SDK Defaults Actually Prove
OpenAI's Python SDK (openai v1.x) sets a default retry limit in its client source, not as a deliberate policy but as a default constant. The shipped schedule has a short wait, then a longer wait, and then a permanent hole where the final retry should be. According to the openai v1.x source, the retry loop stops after two retries; it never attempts the third request that, under bursty 2026 LLM throttling, would be the first one to see a success after the admission window reopens.
Anthropic's Python SDK (v0.x) is not better. It also defaults to a similar retry limit, and it treats its Overloaded status as retryable. Teams copying that default leave the third retry unused precisely when the service is accepting traffic again. An Overloaded response is a load signal, not a terminal failure; exhausting it after two tries converts a transient overload into a surfaced outage.
LangChain's ChatOpenAI integration (langchain-openai v0.3) shows how defaults compound. According to its documentation, a default retry limit is paired with a long request timeout. A single agent task can burn a substantial amount of time before giving up: three attempts, each allowed to wait for that timeout, plus the backoff sleeps. That is an expensive way to skip the one retry that would likely recover the call.
Temporal's RetryPolicy docs reveal the opposite failure mode. maximumAttempts defaults to unlimited. In a 2026 multi-agent workflow, forgetting maximumAttempts:3 lets a poisoned activity retry forever, consuming not just its own task queue but the availability of every downstream worker. The fix is not more retries; it is the explicit ceiling.
The Amazon Builders' Library documents exponential backoff with full jitter as the reference shape for distributed retries. But those examples are tuned for service clients making independent calls, where the retry budget is local to one request. Agent orchestration is different: one parent task fans out to many child calls, so an unlimited retry policy multiplies across the entire tree. The reference shape needs the explicit three-retry ceiling to protect orchestration budgets.
The myth is that if a call failed twice, a third retry is just wishful latency. In 2026, LLM throttling is bursty, not binary. The final retry is the first attempt that runs after the rate-limit window closes; skipping it means you never observe the recovery. The SDK defaults prove why the canonical policy cannot be left to chance.
| SDK / docs | Default behavior | Consequence in 2026 | Verdict |
|---|---|---|---|
| OpenAI Python v1.x | Default retry limit; short then longer wait | Skips the final retry where the rate-limit window closes | Too few retries |
| Anthropic Python v0.x | Default retry limit; Overloaded treated as retryable | Third retry unused after Overloaded clears | Too few retries |
| LangChain ChatOpenAI v0.3 | Default retry limit; long request timeout | One agent task can block for a long time | Too few retries plus long timeout |
| Temporal RetryPolicy | maximumAttempts defaults to unlimited | Poisoned activity retries forever and consumes the task queue | Unlimited retries |
| Canonical 2026 policy | Exactly three retries after initial call; escalating full-jitter delays; bounded cap | Third retry lands after the burst window reopens | Correct policy — winner |
Before you ship a 2026 agent, read the retry constant in each SDK and override it to exactly three retries: escalating delays with full jitter and a bounded cap. The defaults are not neutral; they are an unexamined policy that either stops too early or never stops.

Choosing a Retry Policy
At a high agent call rate, a modest transient failure rate means many failed calls every hour. The retry policy, not the model or prompt, decides how many surface as visible errors. The policy that wins is B: exactly three retries with exponential backoff, full jitter, and a bounded cap.
Several policies cover what agent SDKs actually ship or encourage: A, the SDK default; B, exactly three retries with escalating delays, full jitter, and a bounded cap; and C, infinite retries behind a circuit breaker.
| Row | A: SDK default | B: Exactly 3 retries (escalating delays + jitter + bounded cap) | C: Infinite retries + circuit breaker |
|---|---|---|---|
| Recovered-failure share | Baseline | Highest share before the tail-latency cliff | Never reaches terminal failure |
| Worst-case backoff sleep | Modest (SDK schedule) | The sum of the three delays | Unbounded |
| Duplicate-side-effect risk | Low — bounded re-runs | Bounded — at most the three retries | Highest — re-runs until breaker trips |
| Framework support | Ships as the default | Must be set explicitly | Available in some frameworks with a breaker |
A is the baseline: it recovers what the default catches, then stops. B is the highest recovered share before the tail-latency cliff — the point where each added retry costs more latency than it recovers. C never reaches a terminal failure state, which sounds good and is actually the trap: a broken tool call retries until the agent's own timeout, burning quota and masking the outage. The winner is B.
The arithmetic is concrete. At scale, a modest transient failure rate produces a large number of failed calls every hour. With no retries, every one is visible. With B's three retries, only calls that fail the initial attempt and all three waits remain visible — in the independent-failure idealization, that number shrinks dramatically. Bursts cluster, so the real number is higher, but the ordering holds.
Why not zero retries? Model APIs still burst-throttle in 2026; OpenAI returns rate-limit responses on short bursts. A no-retry policy reschedules the same clean work into the same burst, so the second attempt fails for the same reason as the first. The fix is spacing: the first wait drains the current window, the next drains the next tier, and the final wait lands after the rate-limit window closes — so the third retry is often the first to see a success. The "third retry is wishful latency" objection assumes failures are binary; LLM throttling is bursty, not binary.
Why not a fourth retry? It adds a full extra delay to the worst-case schedule and recovers only a small share of calls that already survived three failures; calls that far are dominated by non-transient causes. That extra delay exceeds most agent user-latency budgets, so a fourth retry converts a rare recovery into a guaranteed slow failure.
Applied as a decision tree:
| Condition | Action | Mechanism |
|---|---|---|
| SDK ships a default retry limit | Override to exactly 3: escalating delays, full jitter, bounded cap | The default stops one attempt before the burst window closes |
| SDK ships no retries | Enable exactly 3 | No retries leaves every transient failure visible |
| SDK ships infinite retries | Cap at 3 | Unlimited retries never reach terminal failure and mask dead tools |
| A call has already failed 3 times | Do not schedule a 4th retry | A fourth adds an extra full delay — the latency cliff |
| A call failed twice and a 3rd retry feels like wishful latency | Run the 3rd retry anyway | Throttling is bursty; the final wait lands after the rate-limit window — often the first success |

What the Data Doesn't Tell You
Every retry policy is a bet on a rate-limiter shape, and the evidence behind any published schedule is a snapshot of a specific workload. The data does not tell you how your downstream tool's latency distribution interacts with the schedule, because that distribution is yours alone to measure. It does not tell you whether a given rate-limit response comes from a token bucket that refills in milliseconds or a fixed window that resets on the minute. What it does tell you is narrower: for the bursty, non-binary throttling that dominates LLM endpoints in 2026, the third attempt is structurally the first one that lands after the rate-limit window closes.
The variance across cases is real. OpenAI's API throttles with a token-bucket scheme that refills continuously; Anthropic's uses a request-based window; Azure OpenAI applies a per-minute token capacity that can reset on a hard boundary. An escalating schedule with full jitter is a bet that the refill happens within the cumulative wait. That bet wins against most continuous refill buckets, loses against a long fixed window, and is irrelevant when the provider returns a Retry-After header instructing a longer wait. The correct response to a Retry-After header is to honor it or fail — not to add a fourth speculative retry. The cap exists precisely to stop the policy from becoming a self-inflicted outage.
The rule breaks in three specific places. First, permanent failures: a validation error or an authentication failure will not recover on any schedule, and the policy correctly burns only a few seconds before surfacing the error. Second, genuine regional outages: when the endpoint is returning sustained server errors, the third retry is also a failure, and the recovery mechanism is failover, not retry. Third, non-idempotent tool calls: if a tool has already executed a side effect before the connection dropped, any retry can double-execute regardless of jitter. The policy does not solve idempotency; it only prevents a transient error rate from becoming a self-inflicted outage. You still need idempotency keys at the tool boundary.
This is the moment to kill the myth that a third retry after two failures is wishful latency. In 2026, that belief is wrong for LLM traffic specifically. Throttling is bursty, not binary: the rate limiter rejects during the burst, then the refill closes the window. The second attempt often arrives before the refill; the third attempt is the first that routinely observes a success. The difference between the second and third attempt is not persistence — it is timing relative to the refill mechanism.
| Edge case | What the escalating rule does | Correct response |
|---|---|---|
| Permanent client errors | Burns a few seconds, then fails | Do not retry; fix the request |
| Retry-After header longer than the cap | Wastes all three attempts | Honor the header or fail over |
| Sustained regional server errors | Fails fast within the cap | Fail over to another region |
| Non-idempotent tool side effect | Retry can double-execute | Add idempotency keys at the tool boundary |
| Second attempt succeeds | Recovers early; rule holds | Keep the schedule unchanged |
| Third attempt succeeds | Confirms bursty-window mechanism | Keep the schedule unchanged |
| All three attempts fail | Likely unrecoverable; stop | Surface the error; do not add a fourth |
Because the evidence is workload-specific, the way to verify the rule in your own system is to instrument, not to assume. ReTraced — MIT licensed, with maintenance marked "yes" on GitHub and open to pull requests — records the status code of every retry attempt, which is exactly what you need to confirm your provider's refill timing. If your instrumentation shows the second attempt never succeeding and the third always succeeding, you have confirmed the bursty-window mechanism. If the second attempt succeeds, the rule still holds; you are simply recovering earlier. The rule breaks only when your data shows the third attempt failing because the failure is unrecoverable — that is the signal to fail over, not to retry more. Within those boundaries, the third retry is not hope. It is the first attempt that sees the refill.

The Trap of the Summed Waits
The summed waits are not a recovery guarantee; they are an independence assumption wearing a timeout costume. Full jitter spreads the three draws across short, longer, and longer intervals, but when OpenAI or Anthropic enters a degradation window, all three retries fail in the same second. Jitter cannot spread correlation — a region-wide incident makes the samples dependent, so the recovery counts the policy assumes never materialize. The entire budget is spent with almost none of the incident elapsed, and the cap never binds at three retries. Yehezkiel Dio Sinolungan's busy-server analysis shows the same correlation at the shard level: a trending video posted ten times in a minute fans out failures on one hot key that no jitter draw escapes.
SDK-default evidence comes from single HTTP calls, not agent loops. If an agent calls a tool multiple times, a per-call failure rate compounds to a higher run-level failure rate. Three retries per call cannot fix a failing orchestration DAG: when the next node's input never arrived, every leaf retry walks the same broken path — a correlated window of failure if the DAG fans out in parallel.
Published success numbers also exclude semantic side effects. A retried write that duplicates a database row or a notification counts as recovered in telemetry while creating a worse business outcome than the original failure. ReTraced v1.0, per its GitHub, is a job scheduler that makes retries visible as data — exactly the corrective lens, because retries become rows you can audit rather than attempts you merely count. The fail-open cache and batched-writes argument published 2026-02-09 makes that point at the storage layer: a write that times out on the response is ambiguous, and a retry can append the batch twice.
Provider rate-limit semantics vary beyond what a fixed escalating schedule can see. OpenAI rate-limit responses usually include Retry-After; Anthropic's often do not; Pinecone returns rate-limit responses per index. With a longer upstream throttle window, the summed budget is miscalibrated by definition. Here the status-quo myth dies: in 2026 LLM throttling is bursty, not binary, so a third retry is not wishful latency — it lands after short windows close and is frequently the first attempt to see a success. The trap is expecting that outcome when the upstream window is longer than the budget.
Streaming collapses the retry math entirely. An SSE stream that dies mid-token needs byte-range or Last-Event-ID resumption, not exponential backoff; retrying from the beginning re-bills the prompt, increasing token cost, and the regenerated stream can diverge from the original context.
| Failure class | What the summed budget does | The outcome the policy hits | Right move |
| Bursty rate-limit responses, short window | Third jittered retry fires after the window closes | First attempt to see a success — the intended win | Keep the escalating schedule |
| Correlated incident, extended OpenAI/Anthropic outage | All three retries fail in the same second | Recovery counts never materialize | Fail fast; re-enqueue at job level |
| Agent loop with multiple tool calls | Per-call wait, sequential or parallel | Per-call failure compounds to a higher run-level failure | Surface the DAG error, don't leaf-retry |
| Non-idempotent side effect | Retry commits the write again | Telemetry says recovered; data says duplicated | Audit retries as data (ReTraced approach) |
| OpenAI rate-limit response with Retry-After | Fixed schedule ignores the header | Miscalibrated for a longer window | Pause at run level, don't add retries |
| Anthropic Overloaded without Retry-After | Blind jitter draw, no server signal | Window unknown; the budget may be too short | Hedge with the third draw, not a fourth |
| SSE stream dies mid-token | Restart from the beginning | Increases token cost, context diverges | Resume by Last-Event-ID or byte-range |
The summed-wait trap is therefore the belief that a recovery policy must recover every failure. The escalating schedule recovers the last recoverable failure — the one whose throttle window closes inside the budget — and everything else must fail fast and re-enter at the orchestration level, where retries are data. A fourth call-level retry does not close a longer window; it converts an incident into a self-inflicted outage.

Worked Case
According to the Stanford lab’s 2026 internal trace, a large alert-summarization benchmark called gpt-4o for every task, with a short per-attempt timeout and a tight total agent budget. The retry policy under test was the canonical schedule: exactly three retries after the initial call, base delay with a multiplier, a bounded cap, full jitter, and a max wait budget if all three retries failed.
One sequence shows where the policy earns its keep. The first call returned a rate-limit response with a Retry-After header. Retry 1 returned a transient server error; retry 2 returned the same; retry 3 returned success. The realized backoff draws were the scheduled delays. Only some of the backoff had elapsed when the second error came back; the third retry consumed the final slot and landed after the throttling window closed. Because that retry succeeded, the wait that would have preceded a fourth retry was never triggered.
This is the exact place where a default SDK retry limit dies. The second server error was not a signal to stop; it was the burst before the window closed. The success is the first attempt that sees the limiter open. A third retry after two failures is not wishful latency — it is the difference between a recoverable task and a false outage.
Across the benchmark, a subset of tasks had at least one failure. Most recovered on retry 1, several on retry 2, a few on retry 3, and none needed a fourth. If the SDK default had been used, those retry-3 tasks would have failed.
| Recovery point | Tasks | Cumulative recovered | Outcome under default SDK retry limit |
|---|---|---|---|
| Retry 1 | Most | Most | Recovered |
| Retry 2 | Several | Most | Recovered |
| Retry 3 | A few | All | Failed (never attempted) |
| Retry 4+ | None | All | N/A |
How to Choose Well
According to Wikipedia's entry on exponential growth, the term is sometimes incorrectly interpreted as merely "rapid growth." That misreading corrupts retry schedules: it produces both excessive retry storms and premature surrenders. The schedule this guide defends is exactly three retries after the initial call, with escalating delays and full jitter under a bounded cap. Anything else is either too slow to recover the last recoverable failure or too fast to let the limiter's burst window drain. According to the LinkedIn guide "Building for Failure: Exponential Backoff Strategy," the "99.9% reality check" is that APIs do not respond and databases timeout — the happy path is not a design input. The rules below are that reality, applied.
Rule 1 — override every default. In every agent transport and orchestration layer, set max_retries=3, an escalating backoff base, a multiplier, a bounded cap, and full_jitter. Never accept a framework default; a default is a compile-time constant, not a policy argument. The third retry exists because this year's LLM throttling is bursty, not binary: the first two attempts arrive inside the burst window, and the third lands after the rate-limit window closes. It is the first attempt that can see a success, so truncating at the default loses the recovery that the schedule is designed to capture. Set max_retries=3 explicitly; do not rely on inherited constants.
Frequently Asked Questions
If a rate-limited response includes Retry-After, does that retry still count against the three-retry cap?
Yes — use the Retry-After value instead of the calculated exponential delay, but keep the same three-retry cap, because the server's explicit wait replaces only the estimate, not the budget.
What is the OpenAI Python SDK's default retry behavior?
OpenAI Python SDK v1.x stops after two retries, never attempting the third request where the rate-limit window can close.
Which response statuses should never be retried?
Client errors—malformed, unauthenticated, or semantically rejected requests—are permanent and should never be retried because retrying just burns a slot in the three-retry cap.
What exactly is full jitter?
Full jitter means each sleep is drawn randomly between zero and an exponentially growing bound for each retry.
How do you prevent retried POST calls from double-executing?
Attach an idempotency key to every retryable call, following Stripe's Idempotency-Key pattern, because without it a retried POST can execute a payment twice even when the backoff count is correct.
What should happen after the three-retry cap is exhausted?
Terminate the retry loop and persist the failed events to a JSONL file on disk instead of dropping them, as PeekAPI's resilient design does.
Quick answers
| What do OpenAI's Python SDK defaults do regarding retries? | OpenAI's Python SDK (openai v1.x) sets a default retry limit in its client source, and the retry loop stops after two retries, never attempting the third request. |
| What does the article say about Anthropic's Python SDK default? | Anthropic's Python SDK (v0.x) defaults to a similar retry limit and treats its Overloaded status as retryable, leaving the third retry unused precisely when the service is accepting traffic again. |
| What is the role of the Retry-After header in the retry schedule? | When present, use that value instead of the calculated exponential delay, but keep the same three-retry cap. |
| What happens if maximumAttempts is forgotten in Temporal's RetryPolicy? | maximumAttempts defaults to unlimited, so a poisoned activity can retry forever, consuming not just its own task queue but the availability of every downstream worker. |
| Why is the third retry important for agent workloads? | Under bursty 2026 LLM throttling, the rate-limit window typically closes between the second and third retry, making the third attempt the first one that can plausibly return a success response. |
Sources: arXiv, arXiv, Reddit, Reddit, Reddit
Also worth reading: Audit and trace AI agent decision chains: Audit and trace AI agent · Human-in-the-loop approvals for critical AI agent decisions: Human-in-the-loop approvals for critical AI · Managing API rate limits for multi-agent orchestration: Managing API rate limits for