On this page
- Retry policy begins with error classification
- Some failures should not be retried unchanged
- 429 is usually a scheduling problem
- Exponential backoff spreads retries over time
- Add jitter to prevent synchronized retries
- Honor explicit server guidance when appropriate
- Retry budgets are better than infinite retries
- A retry budget should include the first request
- Network errors create ambiguous boundaries
- Read-only generation is safer to retry than side effects
- Idempotency makes repeated requests safer
- Your application can own idempotency even when the AI provider does not
- Streaming failures are not ordinary request failures
- Before first output vs after partial output
- Tool-call streaming creates another boundary
- Cancellation should stop the retry loop
- Context errors require rebuilding, not waiting
- Output-limit failures also need semantic recovery
- Authentication failures can sometimes recover after credential refresh
- Retry only at one layer when possible
- Timeouts need a deadline, not only per-attempt limits
- Retry storms can happen inside one app
- Fallback is not the same as retry
- Fallback is safest before side effects
- Fallback after partial text is a product choice
- Background jobs need different retry logic
- Webhook delivery and job creation have independent retries
- Persist retry state when operations survive process restarts
- Observability should show attempts, not only final failure
- Preserve provider request IDs
- Testing retries requires deterministic failures
- Test that non-retryable errors stay non-retryable
- A practical retry decision tree
- A reliable retry checklist
- Where BYOKchat fits
- Further reading
A retry sounds simple:
request failed → send it again
For AI applications, that rule is often wrong.
A generation request may stream partial output, consume tokens, trigger tools, create provider-side state, or complete successfully even though the client never receives the final response. Retrying without understanding the failure boundary can duplicate work, duplicate side effects, increase cost, or produce a second answer that no longer matches the first.
A reliable retry system starts with one question:
Is repeating this operation safe, useful, and still necessary?
Retry policy begins with error classification
Do not treat every non-success response as retryable = true.
A useful high-level classification is:
configuration / authentication error
invalid request
rate limit / capacity pressure
transient provider/server failure
network transport failure
context/output limit issue
cancelled by user
unknown execution outcome
Each category has a different recovery action.
Some failures should not be retried unchanged
Examples:
401 invalid API key
403 insufficient permission
400 unsupported parameter
invalid JSON schema
context window exceeded
model not found
Repeating the identical request usually wastes time and quota.
The request or configuration must change first.
See AI API Errors 401, 403, and 429.
429 is usually a scheduling problem
A rate-limit response means the provider is refusing work because a usage limit or temporary capacity boundary has been reached.
Sending the same request immediately in a tight loop makes the problem worse.
A retry policy should:
- inspect provider-specific retry guidance where available;
- wait before trying again;
- cap attempts;
- preserve user cancellation;
- avoid retry storms across concurrent requests.
Rate limits can apply to different dimensions such as requests, tokens, concurrency, or provider/model-specific capacity. Do not assume every 429 means the same resource was exhausted.
Exponential backoff spreads retries over time
A common retry delay grows after each failure:
where:
- is the initial delay;
- is the retry attempt;
- caps the wait.
For example:
1s → 2s → 4s → 8s → 16s → capped
The exact values are product-specific.
Add jitter to prevent synchronized retries
If 10,000 clients all receive a 429 at the same moment and all retry exactly 8 seconds later, they can create another traffic spike.
Jitter randomizes retry timing.
Conceptually:
delay = backoff + random_component
or a full-jitter strategy can choose a random delay within the current backoff window.
The goal is not randomness for its own sake. It is to avoid coordinated retry waves.
Honor explicit server guidance when appropriate
Providers may return headers or structured error metadata that indicate when to retry or expose current rate-limit state.
When current official provider documentation gives authoritative retry timing, prefer that over inventing a shorter delay.
A multi-provider client should parse this inside the provider adapter and normalize it into something like:
type RetryAdvice = {
retryable: boolean;
retryAfter?: Duration;
category: RetryCategory;
};
Do not make shared UI code parse provider-specific HTTP headers.
Retry budgets are better than infinite retries
Define limits such as:
maximum attempts
maximum total retry time
wall-clock deadline
maximum cumulative token/cost exposure
A bounded policy might say:
retry transient pre-stream failure up to 3 attempts
within 30 seconds total
unless user cancels
The exact numbers depend on the product, but the existence of a budget is important.
A retry budget should include the first request
Be clear about terminology:
attempt 1 = original request
attempt 2 = first retry
attempt 3 = second retry
This prevents off-by-one behavior where maxRetries = 3 unexpectedly causes four total provider calls.
Network errors create ambiguous boundaries
Suppose the client sends a request and the socket closes.
There are at least three possibilities:
provider never received request
provider received request but did not start work
provider completed work but response was lost
The client may not know which occurred.
This is an unknown outcome, not necessarily a clean failure.
Read-only generation is safer to retry than side effects
A plain text-generation request may be acceptable to repeat, though it can still create duplicate cost or provider state.
A tool-enabled request can be much riskier.
Imagine:
model calls send_email
email server sends successfully
network dies before tool result reaches orchestrator
Retrying the entire agent turn could send the email again.
Tool execution changes retry semantics fundamentally.
See How AI Tool Calling Works.
Idempotency makes repeated requests safer
An idempotent operation can be repeated without applying the same logical side effect multiple times.
A classic pattern is an application-generated idempotency key:
operation_id = "send-invoice-42-v1"
The receiving service records that key and returns the original result when the same logical operation is submitted again.
This is especially valuable for:
- payments;
- email sends;
- issue creation;
- file writes;
- job creation;
- other mutating tool calls.
Provider/API support varies, so do not assume every endpoint honors an idempotency header merely because another API does.
Your application can own idempotency even when the AI provider does not
For custom tools, the orchestrator can create its own durable execution ID:
tool_execution_id = exec_123
Then the tool backend can deduplicate on that ID.
The model should not generate the idempotency identity. The application does.
Streaming failures are not ordinary request failures
A stream can fail after the user already saw output:
"The migration requires three steps. First..."
[connection lost]
Blindly retrying and replacing the text can produce a completely different continuation.
The application needs a state such as:
partial_response
and a deliberate UX/recovery choice.
Before first output vs after partial output
A useful retry boundary is:
failure before any semantic output
→ automatic retry may be invisible and acceptable
failure after partial output
→ preserve partial response and expose recovery explicitly
This is not a universal law, but it prevents surprising rewrites of content the user already read.
Tool-call streaming creates another boundary
A stream may have emitted a complete tool call before failing.
If that tool has already executed, restarting the model turn can duplicate side effects.
Track separately:
model stream state
tool-call assembly state
tool execution state
tool result delivery state
Do not infer all of them from whether the HTTP stream is open.
Cancellation should stop the retry loop
If the user taps Stop, pending backoff timers must not silently fire later and restart generation.
Propagate cancellation through:
scheduled retry
network request
stream consumer
tool execution where cancellable
background status polling
A retry system that ignores cancellation feels broken even when technically reliable.
Context errors require rebuilding, not waiting
If the provider rejects a request because it is too large, exponential backoff changes nothing.
The correct recovery is:
reduce/compact context
→ rebuild request
→ submit again
See How to Design Context Management for Long AI Conversations.
Output-limit failures also need semantic recovery
If generation stops because an output limit is reached, restarting from scratch may not be what the user wants.
Depending on provider/model semantics, options include:
- continue from supported provider state;
- ask model to continue with preserved context;
- increase configured output budget where valid;
- split the task;
- present the partial result.
Classify truncation separately from network/transient failure.
Authentication failures can sometimes recover after credential refresh
A 401 caused by an expired OAuth access token may be recoverable by refreshing credentials and repeating the request.
A 401 caused by a deleted API key is not.
This is why HTTP status alone is insufficient.
The provider/authorization layer should determine whether credential recovery succeeded before retrying the AI request.
Retry only at one layer when possible
A common bug is stacked retry policies:
HTTP library retries 3 times
provider SDK retries 3 times
application retries 3 times
Worst case, one user action creates many more calls than expected.
Know which layer owns retries.
If an SDK retries automatically, account for those attempts in observability and application-level policy.
Timeouts need a deadline, not only per-attempt limits
Suppose each attempt has a 30-second timeout and you allow five attempts.
The user may wait far longer than expected.
Use an overall deadline:
operation_deadline = start + 60 seconds
Before each retry:
if nextDelay + estimatedAttemptTime > remainingDeadline:
stop retrying
This keeps latency bounded.
Retry storms can happen inside one app
Imagine 50 active chats all hit the same provider rate limit.
Independent exponential-backoff loops can still create substantial pressure.
A provider-level scheduler can coordinate:
- concurrency limits;
- shared cooldown after strong rate-limit signals;
- request queues;
- cancellation;
- priority.
Rate limiting is often better solved above individual requests.
Fallback is not the same as retry
A retry says:
try the same logical operation again
Provider/model fallback says:
try the logical operation through a different execution path
Fallback can change:
- model behavior;
- tool-call schema;
- reasoning semantics;
- context window;
- safety behavior;
- structured-output capability;
- cost;
- latency.
Treat fallback as routing, not merely a faster retry.
See Reliable AI Provider Fallback and Model Routing.
Fallback is safest before side effects
A good boundary is:
provider fails before any tool/side effect
→ fallback may be feasible
provider fails after external mutation
→ fallback requires careful continuation/reconciliation
If Provider A already caused a tool to create an issue, Provider B must not restart the turn as though nothing happened.
Fallback after partial text is a product choice
If Provider A emitted half an answer, switching to Provider B can produce inconsistent voice or content.
Possible choices:
- keep partial output and offer regenerate;
- discard partial output and clearly restart;
- continue only if portable context/state supports it;
- avoid automatic fallback after visible output.
Do not hide a provider switch that materially changes the user’s response without a coherent UX policy.
Background jobs need different retry logic
Once a provider has returned a durable background job/response ID, status-fetch failures should not recreate the job.
Retry:
GET/status retrieval
not:
POST create new job
This distinction prevents duplicate long-running work.
See How Long-Running AI Tasks Work.
Webhook delivery and job creation have independent retries
A webhook handler may fail after the provider job already completed.
The correct recovery is to retry/reconcile event processing, not regenerate the model response.
Long-running systems should identify the operation whose failure is being retried.
Persist retry state when operations survive process restarts
For durable workflows, record:
attempt count
last error category
next allowed attempt time
operation deadline
provider request/job identity
side-effect checkpoint
Otherwise an app restart can reset retry budgets and accidentally exceed intended limits.
Observability should show attempts, not only final failure
Useful metrics include:
total logical requests
provider attempts per logical request
retry count by error category
rate-limit frequency
transient failure frequency
retry recovery rate
fallback rate
unknown-outcome count
time spent in backoff
A system can have a 99.9% final success rate while silently retrying every request three times. That is still a reliability and cost problem.
Preserve provider request IDs
Providers often return request/trace identifiers in headers or error metadata.
Store safe identifiers in diagnostics so you can correlate a failure with provider support/logging without recording prompts or credentials.
This is especially valuable when a transient issue affects only certain requests or regions.
Testing retries requires deterministic failures
Real providers are poor test harnesses because you cannot reliably make them fail at exactly the right point.
Use a simulator/fake adapter that can produce:
429 then success
500 twice then success
network drop before headers
network drop after first text delta
stream ends mid-tool arguments
tool succeeds then result delivery fails
context-too-long
invalid key
background job accepted then status endpoint fails
Then assert exact attempt counts and side effects.
Test that non-retryable errors stay non-retryable
A retry suite should verify negative behavior:
invalid API key → one attempt
invalid request → one attempt
user cancellation → no later retry
unsafe unknown side effect → no automatic replay
Preventing a retry is often more important than performing one.
A practical retry decision tree
The key step is Safe to repeat?, not the backoff formula.
A reliable retry checklist
Before enabling automatic retries, verify that:
- errors are classified before retrying;
- invalid/configuration failures do not loop;
- provider retry guidance is respected where available;
- exponential backoff includes jitter;
- total attempts and wall-clock time are bounded;
- user cancellation cancels queued retries;
- unknown outcomes are reconciled before mutating replay;
- tool side effects use idempotency where possible;
- streaming failures preserve partial-output semantics;
- background status checks do not recreate jobs;
- fallback is treated separately from retry;
- SDK-level and app-level retries do not multiply unexpectedly;
- observability counts provider attempts per logical request;
- deterministic failure simulators cover each boundary.
Where BYOKchat fits
A multi-provider client benefits from a shared reliability layer that understands logical requests, deadlines, cancellation, and side-effect boundaries. Each provider adapter can normalize native errors and retry hints without forcing the rest of the app to know every provider’s status codes and headers.
That makes retry behavior consistent without pretending all providers have identical failure semantics.