Claude Code Subagent Error Recovery in 2026: Retry Logic, Fallback Strategies, and Detecting Stuck Loops

This article was written with the assistance of AI, under human supervision and review.

Most production agent failures trace back to a single omission: the absence of recovery logic. A subagent hits a rate limit, retries the same call immediately, burns through budget, and crashes the parent workflow. The coordinator receives a cryptic timeout after 90 seconds and has no idea which step failed or why.

Effective error recovery classifies failures by type, applies exponential backoff to transient issues, detects stuck loops before they drain budget, and propagates only the errors that require coordinator intervention. The difference shows up in uptime metrics and cost reports within days.

Key Takeaways

  • Transient failures (rate limits, network blips) require retry logic with exponential backoff; logical failures (invalid parameters) need immediate propagation.
  • Step repetition loops, where an agent retries identical tool calls, account for 15.7% of production failures and require hash-based detection.
  • Circuit breakers prevent runaway costs by halting agents that exceed token budgets or attempt thresholds before they exhaust resources.
  • Multi-agent systems should implement local recovery at the subagent level and propagate only errors the coordinator can resolve.
  • Fallback strategies like model switching or degraded responses keep workflows running when primary approaches fail.

Error Classification: Transient vs Logical vs Fatal

Error recovery begins with classification. Not all failures deserve the same response. A rate limit from the Anthropic API is transient and recoverable. A schema validation error in tool parameters is logical and permanent. A memory exhaustion crash is fatal and requires process restart.

Transient errors include network timeouts, temporary service unavailability, and quota limits. These failures resolve with time or retry attempts. Retry logic with exponential backoff handles them without human intervention.

Logical errors stem from incorrect inputs, malformed requests, or violated preconditions. The agent passed an invalid file path to a tool. The database query referenced a nonexistent column. Retrying the same call will produce the same failure. These errors require immediate propagation to the coordinator or user.

Fatal errors signal systemic problems: out-of-memory conditions, corrupted state, unrecoverable resource exhaustion. Recovery at the agent level is impossible. The process must terminate gracefully and report the failure upstream.

Teams that skip classification treat all errors identically. They retry logical failures until a timeout expires, wasting budget on calls guaranteed to fail. They give up on transient failures after a single attempt, reporting unavailability when a two-second wait would have succeeded.

The classification layer sits at the entry point of your error handler. Inspect the error type, status code, or message pattern. Route transient errors to retry logic, logical errors to validation logging, and fatal errors to graceful shutdown.

Building Retry Logic with Exponential Backoff

Exponential backoff prevents retry storms that amplify outages. When a rate limit hits, immediate retries pound the API harder and extend the unavailability window. Each retry doubles the wait time: 1 second, 2 seconds, 4 seconds, up to a maximum delay.

The implementation tracks attempt count, calculates delay with jitter, and enforces a maximum retry limit:

interface RetryConfig { maxAttempts: number; baseDelayMs: number; maxDelayMs: number; jitterFactor: number; } async function withRetry<T>( operation: () => Promise<T>, config: RetryConfig, isRetryable: (error: unknown) => boolean ): Promise<T> { let attempt = 0; while (attempt < config.maxAttempts) { try { return await operation(); } catch (error) { attempt++; if (!isRetryable(error) || attempt >= config.maxAttempts) { throw error; } const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt - 1); const jitter = exponentialDelay * config.jitterFactor * Math.random(); const delayMs = Math.min(exponentialDelay + jitter, config.maxDelayMs); await new Promise(resolve => setTimeout(resolve, delayMs)); } } throw new Error("retry logic exhausted without success or terminal error"); } function isTransientError(error: unknown): boolean { if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes("rate limit") || message.includes("timeout") || message.includes("503") || message.includes("temporarily unavailable") ); } return false; } const result = await withRetry( () => callClaudeAPI(prompt), { maxAttempts: 5, baseDelayMs: 1000, maxDelayMs: 16000, jitterFactor: 0.3 }, isTransientError ); 

Jitter randomizes delays across concurrent agents. Without jitter, all failed agents retry at identical intervals and create synchronized load spikes. Jitter spreads retries across a time window.

The isRetryable predicate prevents retry loops on logical errors. Check the error message, status code, or custom error class. Return false for validation failures, authentication errors, or missing resources.

Exponential backoff visualization showing delay curves with and without jitter

Exponential backoff visualization showing delay curves with and without jitter

Teams that omit retry logic lose uptime to momentary glitches. A single network hiccup terminates the workflow. Teams that retry without backoff amplify failures and rack up costs during outages.

Detecting and Breaking Stuck Loops: The Step Repetition Problem

Step repetition loops occur when an agent retries identical tool calls without recognizing the pattern. The agent requests a file that does not exist, receives an error, and immediately requests the same file with the same path. After 20 iterations, the budget is exhausted and no progress has occurred.

Detection requires hashing each attempted step and tracking recent history. If the same hash appears three times in the last five steps, the agent is stuck:

interface StepAttempt { toolName: string; parameters: Record<string, unknown>; timestamp: number; } class LoopDetector { private history: string[] = []; private maxHistorySize = 10; private repetitionThreshold = 3; private lookbackWindow = 5; recordStep(step: StepAttempt): void { const hash = this.hashStep(step); this.history.push(hash); if (this.history.length > this.maxHistorySize) { this.history.shift(); } } isStuck(): boolean { if (this.history.length < this.repetitionThreshold) { return false; } const recentSteps = this.history.slice(-this.lookbackWindow); const latestHash = recentSteps[recentSteps.length - 1]; const matchCount = recentSteps.filter(h => h === latestHash).length; return matchCount >= this.repetitionThreshold; } private hashStep(step: StepAttempt): string { const normalized = JSON.stringify({ tool: step.toolName, params: step.parameters }); return this.simpleHash(normalized); } private simpleHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return hash.toString(36); } reset(): void { this.history = []; } } const detector = new LoopDetector(); for (const step of agentSteps) { detector.recordStep(step); if (detector.isStuck()) { throw new Error( `agent stuck in repetition loop: attempted ${step.toolName} ` + `with identical parameters ${detector["repetitionThreshold"]} times` ); } await executeStep(step); } 

The hash function normalizes tool name and parameters to catch semantic equivalence. Two calls with differently ordered JSON keys but identical content produce the same hash.

When a loop is detected, the agent must either request human intervention, switch to a fallback approach, or terminate the workflow with a descriptive error. Continuing execution wastes resources and delays failure diagnosis.

Some loops arise from environment state. The agent checks if a file exists, finds it missing, attempts to create it, and the creation fails silently. The next iteration repeats the check and creation. Loop detection surfaces the underlying issue: file creation permissions are missing.

Teams that ignore loop detection discover stuck agents hours later when budget alerts fire. The agent consumed $200 in tokens retrying a single failed operation. The workflow never progressed past step two.

Fallback Strategies: Model Switching and Degraded Responses

When primary recovery fails, fallback strategies keep workflows running at reduced capacity. Model switching drops to a cheaper or faster model when the primary model is unavailable. Degraded responses return partial results instead of failing completely.

Model switching maintains a priority list. If Claude 3.5 Sonnet hits rate limits, the coordinator switches to Claude 3 Haiku for less critical subagents. The workflow continues with reduced accuracy but meets the deadline:

interface ModelConfig { modelId: string; priority: number; costPerToken: number; maxTokens: number; } class ModelFallbackStrategy { private models: ModelConfig[] = [ { modelId: "claude-3-5-sonnet-20241022", priority: 1, costPerToken: 0.003, maxTokens: 200000 }, { modelId: "claude-3-5-haiku-20241022", priority: 2, costPerToken: 0.0008, maxTokens: 200000 }, { modelId: "claude-3-haiku-20240307", priority: 3, costPerToken: 0.00025, maxTokens: 200000 } ]; private currentIndex = 0; async callWithFallback<T>( operation: (modelId: string) => Promise<T> ): Promise<T> { while (this.currentIndex < this.models.length) { const model = this.models[this.currentIndex]; try { return await operation(model.modelId); } catch (error) { if (this.shouldFallback(error) && this.currentIndex < this.models.length - 1) { console.warn( `model ${model.modelId} failed, falling back to ${this.models[this.currentIndex + 1].modelId}` ); this.currentIndex++; } else { throw error; } } } throw new Error("all fallback models exhausted"); } private shouldFallback(error: unknown): boolean { if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes("rate limit") || message.includes("overloaded") || message.includes("503") ); } return false; } reset(): void { this.currentIndex = 0; } } const strategy = new ModelFallbackStrategy(); const response = await strategy.callWithFallback(async (modelId) => { return await anthropic.messages.create({ model: modelId, max_tokens: 4096, messages: [{ role: "user", content: prompt }] }); }); 

Degraded responses return partial data when complete results are unavailable. A document summarization agent that cannot access three of ten source files returns a summary of the seven it processed, along with a warning about missing sources.

The key decision: when does partial success outweigh complete failure? If the workflow is time-critical and 70% accuracy suffices, degraded responses are appropriate. If correctness is paramount, fail fast and propagate the error.

Model fallback cascade showing priority tiers from Sonnet to Haiku

Model fallback cascade showing priority tiers from Sonnet to Haiku

Teams that lack fallback strategies experience all-or-nothing availability. A single rate limit brings down the entire multi-agent system. Teams with fallback strategies maintain partial functionality during degraded conditions and meet deadlines that rigid systems miss.

Error Propagation in Multi-Agent Systems

Subagents should recover locally from transient failures before propagating errors to the coordinator. Only errors that require coordinator intervention or represent unrecoverable logical failures should propagate upward.

A file processing subagent that encounters a rate-limited API call retries with exponential backoff. If retries succeed, the coordinator never learns about the transient failure. If all retries fail, the subagent propagates a structured error with context:

Error objects must carry sufficient context for the coordinator to make informed decisions. Include the subagent ID, the failed operation, relevant parameters, and prior recovery attempts:

interface SubagentError { subagentId: string; operation: string; parameters: Record<string, unknown>; originalError: string; retryAttempts: number; recoveryStrategy: string; timestamp: number; } class Subagent { private id: string; private retryConfig: RetryConfig; async executeTask(task: Task): Promise<TaskResult> { try { return await withRetry( () => this.performTask(task), this.retryConfig, isTransientError ); } catch (error) { const structured: SubagentError = { subagentId: this.id, operation: task.operation, parameters: task.parameters, originalError: error instanceof Error ? error.message : String(error), retryAttempts: this.retryConfig.maxAttempts, recoveryStrategy: "exponential backoff", timestamp: Date.now() }; throw structured; } } private async performTask(task: Task): Promise<TaskResult> { // actual task execution return { status: "completed", data: {} }; } } class Coordinator { async orchestrate(subagents: Subagent[], tasks: Task[]): Promise<void> { const results = await Promise.allSettled( tasks.map((task, i) => subagents[i].executeTask(task)) ); for (const result of results) { if (result.status === "rejected") { const error = result.reason as SubagentError; if (this.canCompensate(error)) { await this.applyCompensation(error); } else { throw new Error( `unrecoverable subagent failure: ${error.subagentId} ` + `failed ${error.operation} after ${error.retryAttempts} attempts` ); } } } } private canCompensate(error: SubagentError): boolean { return error.operation === "fetch_optional_data"; } private async applyCompensation(error: SubagentError): Promise<void> { console.log(`compensating for optional operation failure: ${error.operation}`); } } 

The coordinator inspects each error and decides whether to retry with a different subagent, apply a compensation strategy, or fail the entire workflow. Optional operations that fail may warrant compensation rather than termination.

Correlation IDs link errors across agent boundaries. When a subagent propagates an error, it includes the correlation ID from the originating request. The coordinator can trace the failure back through the call chain and identify the root cause. For more on correlation IDs in agent systems, see correlation IDs for AI agents.

Teams that propagate every error overwhelm coordinators with noise. Transient failures that resolve locally should remain invisible. Teams that swallow all errors at the subagent level leave coordinators blind to systemic issues.

Circuit Breakers and Budget Caps for Runaway Agents

Circuit breakers prevent runaway agents from exhausting budgets or API quotas. When an agent exceeds a token threshold or failure count, the circuit breaker trips and halts execution before damage compounds.

Token budget tracking measures...