Earlier posts covered semantic recovery inside the agent loop: raising the output-token limit, compacting an overlong context, and handling stop reasons. A production agent also encounters infrastructure failures:
- The network fails before a request reaches the API.
- The API returns a 500 server error.
- A 429 rate limit is reached.
- A 529 reports an overloaded Anthropic cluster.
- The API rejects the request as
prompt_too_long.
Claude Code does not handle those cases with one generic retry. It layers retries, server-directed backoff, model fallback, context recovery, and hard stop conditions so that a single user request has the best possible chance of finishing without manual intervention.
Every API call goes through withRetry
Claude Code never makes a bare model request. Every logical call_llm is wrapped by withRetry, so one apparent call may be one to ten actual HTTP requests.
Its core behavior is:
-
DEFAULT_MAX_RETRIES = 10. -
CLAUDE_CODE_MAX_RETRIEScan override that default. - On failure, decide whether the error is retryable.
- If retryable, wait according to the backoff policy and send the request again.
- If not retryable, return the failure to the outer loop immediately.
The main loop therefore sees an API call as either a final success or a final failure. The transient request attempts remain hidden inside withRetry.
Retryability is server-directed first
The naïve approach is to classify errors entirely in the client: retry 500 and 429, never retry 400.
Claude Code instead asks the server first. Anthropic error responses can include:
x-should-retry: true
or:
x-should-retry: false
If that header is present, it takes precedence over the HTTP status. The client uses its own status-code, error-type, and response-body heuristics only when the header is absent—for example, an overloaded_error in the body can still be recognized as retryable.
This is a production-friendly pattern. The service knows more about its current cluster state and can change retry semantics without requiring every client to ship an update. Client heuristics remain a useful fallback, not the authority.
Backoff is server-directed first, too
Once a failure is retryable, the next question is when to retry.
Claude Code prefers Retry-After from the response:
-
Retry-After: 30means wait 30 seconds. - An HTTP-date
Retry-Aftervalue means wait until that time. - Without either, use exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, and so on.
The server can often provide the exact rate-limit reset time. A client-side exponential schedule is blind: it may wait 32 seconds when the service recovers in five, or retry after eight when the limit is still active. Server-directed delays reduce both waste and unnecessary repeat failures.
For some rate limits, persistent retry waits directly for the rate-limit window to reset instead of applying exponential backoff. That is the most precise schedule when a known quota window has been exhausted.
529 overload gets special treatment
Anthropic's HTTP 529 means the cluster is overloaded: resources are unavailable for the request right now. It differs from 429, which means the caller is sending too much traffic.
Blindly retrying a 529 can amplify the incident. If every caller retries as soon as it receives an overload response, aggregate pressure rises rather than falls—the classic retry storm.
Claude Code distinguishes query sources:
- Non-foreground work such as compaction or session-memory queries fails immediately rather than retrying. These background tasks are not worth intensifying cluster pressure.
-
Foreground work may retry because the user is actively waiting, but only up to
MAX_529_RETRIES.
After repeated 529 failures, Claude Code raises FallbackTriggeredError, which gives the next layer a chance to recover by changing models.
Fallback model swap preserves the current turn
When a primary model cannot recover from an overload or another fatal error, Claude Code can switch to a fallback model:
claude-opus-4-6 hits repeated 529 errors
↓
FallbackTriggeredError
↓
switch to a configured fallback, such as claude-sonnet-4-6
↓
resend the same message history
The swap does not count as a new turn. It happens inside the state machine's inner fallback loop, not its outer loop that increments turnCount.
That matches the user experience: pressing Enter once should represent one interaction. An automatic resilience mechanism should not consume the user's maxTurns allowance.
Stripping thinking signatures before a swap
Thinking signatures are model-specific. A historical assistant message may contain something like:
{ type: 'thinking', signature: '...', thinking: '...' }
Sending a signature produced by one model to another can cause a server rejection. Before the fallback request, Claude Code scans the message history and removes signature fields while keeping the thinking content. The conversation remains understandable, but incompatible metadata does not cross the model boundary.
prompt_too_long: a three-stage recovery
Part 4 discussed model_context_window_exceeded as a stop reason. The API can also reject a request directly with prompt_too_long, which requires a stronger recovery path.
Claude Code tries three stages:
- Context-collapse drain — aggressively remove a large amount of old history. This mechanism is feature-flagged and more forceful than ordinary compaction.
-
Reactive compaction — run the standard
/compact-style process because the API has already rejected the prompt. -
Surface the error — if both attempts fail, return
{ reason: 'prompt_too_long' }to the SDK layer.
Crucially, these intermediate failures are withheld from SDK consumers. The caller does not see a prompt-too-long error that is later repaired; it sees one only after the final recovery stage fails. This is the recovery-engine principle from Part 5: The QueryEngine Main Loop as a State Machine.
max_tokens also receives three chances
The max_tokens stop reason follows a similar escalating sequence:
- On the first limit hit, increase the
max_tokensceiling and retry. - If the raised ceiling is reached too, append
[Output token limit hit, continue]as a user message so the model explicitly resumes unfinished work. - If it still hits the limit, stop recovery and surface the error.
The recovery limit is MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3. The loop keeps giving itself a structured opportunity to finish before involving the user.
The eight-layer recovery stack
From the nearest request-level mechanism to the outermost guardrail, a failed model call can pass through eight layers:
-
withRetryretries network, 500, and eligible 529 failures with backoff, up to ten times. - Fallback model swap tries another model when the primary cannot recover.
- Signature stripping keeps fallback requests compatible with a different model's thinking format.
-
Three-stage
prompt_too_longrecovery progresses from collapse to compaction to a surfaced error. -
Three-stage
max_tokensrecovery escalates the limit, injects continuation, then surfaces an error. - State-machine transitions such as reactive compaction, Stop-hook blocking, and output recovery route work through the next iteration.
-
maxTurnsis the hard circuit breaker when every preceding attempt still fails to reach a terminal result. - Error withholding prevents SDK callers from seeing transient failures; only unrecoverable terminal failures reach the user.
Together, these layers mean that one press of Enter is given as many safe, meaningful recovery paths as possible. Human intervention is reserved for the cases the agent truly cannot repair.
Takeaways
- Every API request uses
withRetry, with up to ten attempts by default. -
x-should-retryandRetry-Afterlet the server lead retry and backoff decisions. - 529 overloads treat background work and foreground user work differently to avoid retry storms.
- Model fallback preserves the current turn and removes incompatible thinking signatures.
-
prompt_too_longandmax_tokensboth use staged recovery instead of one-shot failure. - Eight nested resilience layers make Claude Code a recovery engine rather than a simple error handler.
The next article examines the other endpoint of recovery: when the user actively interrupts the loop. What happens to an in-flight stream or running tool after Ctrl-C, and how does the system keep message history structurally valid?
References
Primary source locations (v2.1.220):
-
src/services/api/withRetry.ts:withRetry,DEFAULT_MAX_RETRIES, andshouldRetry. -
src/services/api/errors.ts: error classification andgetErrorMessageIfRefusal. -
src/query.ts:attemptWithFallback, model swaps, andprompt_too_longrecovery. -
src/query.ts:truncateHeadForPTLRetryand the three-stage prompt-length path. -
src/services/compact/compact.ts: reactive-compaction entry points.
Related reading:
Top comments (0)