◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-03-27Advanced

Claude API Production Resilience Patterns — Model Routing, Circuit Breakers, and Fallback Strategies for Indie Teams

Production resilience patterns for Claude API: circuit breakers, intelligent model routing, fallback chains, exponential backoff with jitter, and disaster recovery — with TypeScript implementations and operational notes from an indie developer.

api39production111resilience10circuit-breaker4retry7fallback9error-handling11typescript11

✦ Premium Article

The overnight batch job had stopped, and I only found out the next morning when I opened the logs. A 529 came back, a retry fired, that retry drew another 529, and a whole night of work was gone. Retries and timeouts were already in the code. They were there, and they were not enough.

Rate limits, slow responses, a brief upstream outage — none of that is avoidable. What is avoidable is the moment a stall turns into an avalanche.

What follows are the five patterns I run to stop that avalanche: circuit breakers, intelligent model routing, fallback chains, backoff with full jitter, and disaster recovery. Each one comes with TypeScript you can paste into a Worker, and with numbers I measured rather than assumed.

Why LLM Applications Need Specialized Resilience Engineering

LLM applications face unique failure modes that traditional web API resilience patterns don't fully address.

First, there's response time unpredictability. Claude API response times vary dramatically — from a few hundred milliseconds for simple queries to tens of seconds for complex reasoning tasks. Static timeouts simply don't work here.

Second, rate limit cascading is a real threat. When one request hits a rate limit, subsequent requests pile up, creating a cascade effect that can cripple your entire system's throughput within seconds.

Third, there's the cost explosion risk. A naive retry strategy during an outage can generate thousands of wasted API calls, inflating your bill dramatically with zero value delivered to users.

By combining the five patterns covered in this article, you can build a system that gracefully handles all of these scenarios while maintaining high availability for your users. If you need a refresher on foundational error handling, check out Claude API Error Handling and Retry Strategies first — this guide builds on those basics with production-grade patterns.

Implementing the Circuit Breaker Pattern

The circuit breaker is your first line of defense against cascading failures. Like an electrical breaker, it monitors for consecutive failures and automatically cuts off requests to a failing service, giving it time to recover. It operates in three states: Closed (normal), Open (blocking requests), and Half-Open (testing recovery).

// circuit-breaker.ts — Circuit breaker implementation for Claude API
interface CircuitBreakerConfig {
  failureThreshold: number;    // Failures before opening the circuit
  resetTimeout: number;        // Time before attempting recovery (ms)
  halfOpenMaxAttempts: number;  // Max attempts in half-open state
  monitorWindow: number;       // Failure counting window (ms)
}
 
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
 
class ClaudeCircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failureCount = 0;
  private lastFailureTime = 0;
  private halfOpenAttempts = 0;
  private successCount = 0;
 
  constructor(
    private config: CircuitBreakerConfig = {
      failureThreshold: 5,
      resetTimeout: 30_000,
      halfOpenMaxAttempts: 3,
      monitorWindow: 60_000,
    }
  ) {}
 
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // Open state: reject requests immediately
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime >= this.config.resetTimeout) {
        this.transitionTo('HALF_OPEN');
      } else {
        throw new CircuitOpenError(
          `Circuit is OPEN. Retry after ${this.remainingResetTime()}ms`
        );
      }
    }
 
    // Half-open state: limit probe attempts
    if (this.state === 'HALF_OPEN' &&
        this.halfOpenAttempts >= this.config.halfOpenMaxAttempts) {
      this.transitionTo('OPEN');
      throw new CircuitOpenError('Half-open attempts exhausted');
    }
 
    try {
      if (this.state === 'HALF_OPEN') this.halfOpenAttempts++;
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
 
  private onSuccess(): void {
    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      // Two consecutive successes in half-open → transition to CLOSED
      if (this.successCount >= 2) {
        this.transitionTo('CLOSED');
      }
    }
    this.failureCount = 0;
  }
 
  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();
 
    if (this.state === 'HALF_OPEN') {
      // Any failure in half-open → back to OPEN
      this.transitionTo('OPEN');
    } else if (this.failureCount >= this.config.failureThreshold) {
      this.transitionTo('OPEN');
    }
  }
 
  private transitionTo(newState: CircuitState): void {
    console.log(`[CircuitBreaker] ${this.state} → ${newState}`);
    this.state = newState;
    if (newState === 'CLOSED') {
      this.failureCount = 0;
      this.successCount = 0;
    } else if (newState === 'HALF_OPEN') {
      this.halfOpenAttempts = 0;
      this.successCount = 0;
    }
  }
 
  private remainingResetTime(): number {
    return Math.max(
      0,
      this.config.resetTimeout - (Date.now() - this.lastFailureTime)
    );
  }
 
  getState(): CircuitState {
    return this.state;
  }
}
 
class CircuitOpenError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'CircuitOpenError';
  }
}
 
// Usage example
const breaker = new ClaudeCircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30_000,   // Retry after 30 seconds
  halfOpenMaxAttempts: 3,
  monitorWindow: 60_000,
});
 
const result = await breaker.execute(async () => {
  return await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Hello' }],
  });
});
// Expected: API response on success, CircuitOpenError during outages

The key detail here is the monitorWindow configuration. By only counting failures within a sliding time window, you prevent old, isolated failures from permanently biasing the circuit breaker's state.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Concrete circuit breaker thresholds (5 errors / 30s) and per-usecase tuning tables that beat the textbook defaults
✦Cloudflare Workers CPU-time constraints that force you to split ResilienceManager across a Cron Triggers Worker and Durable Objects
✦How to derive RTO/RPO from revenue, and when swapping the Supervisor LLM from Sonnet to Haiku keeps costs down
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-04-23
High-Availability Patterns for the Claude API — Making Sonnet/Haiku/Opus Fallback Work in Production
A single-model Claude API integration will fall over the first time rate limits or a regional hiccup land at peak hours. This is the production pattern for a Sonnet → Opus → Haiku fallback chain, with circuit breakers, streaming coverage, and the pitfalls you only learn the hard way.
⬡ API & SDK2026-05-22
Designing a Model-Selection Fallback That Survives this model is currently unavailable on Claude API
When Claude API answers 'this model is currently unavailable', treating it like a 529 Overloaded or a rate limit sends you the long way round. Here is how I tell the rejections apart by SDK exception class, switch model IDs per request, and the assumptions I had to fix along the way.
⬡ API & SDK2026-05-22
Why tool_result could not be submitted Keeps Coming Back, and How to Build a Recovery Handler That Actually Holds
Run a Claude agent long enough and one day it starts: 'tool_result could not be submitted', back to back, and retries change nothing. The error message hides four completely different root causes. Here is what I learned debugging it on the always-on agent jobs I run as an indie developer, with the TypeScript recovery handler I now ship in production.