Resilient LLM Calls in TypeScript: Retries, Breakers, Fallbacks Priya Nair published a tutorial showing how to wrap Anthropic's Claude API calls in TypeScript with Cockatiel retries and a circuit breaker that fails over from Claude Opus 5 to Claude Sonnet 5, using about 70 lines of code. The implementation uses Cockatiel 4.0.0 and @anthropic-ai/sdk 0.122.0, with retry policy set to 3 attempts after the first call, exponential backoff starting at 500ms capped at 8s, and a circuit breaker that opens after 5 consecutive transient failures and allows a probe after 30 seconds. Resilient LLM Calls in TypeScript: Retries, Breakers, Fallbacks Wrap Claude API calls with Cockatiel retries and a circuit breaker that fails over to a second model. Priya Nair https://sourcefeed.dev/u/priya nair 1. What you'll build An ask prompt function that calls Claude Opus 5 with exponential-backoff retries, trips a circuit breaker after repeated failures, and answers from Claude Sonnet 5 whenever the primary model is overloaded, rate-limited, unreachable, or gone. About 70 lines, built on Cockatiel https://github.com/connor4312/cockatiel and the Anthropic TypeScript SDK https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript . 2. Prerequisites Node.js https://nodejs.org/ 24 LTS verified on 24.20.0 . Node runs .ts files directly since 22.18, so there's no build step. Cockatiel 4 requires Node 22 or newer. @anthropic-ai/sdk 0.122.0 and cockatiel 4.0.0, the current releases as of this writing. typescript 7.0.2 and @types/node , used only for tsc --noEmit ; Node strips the types at runtime.- An Anthropic API key exported as ANTHROPIC API KEY , with access to claude-opus-5 and claude-sonnet-5 . - Commands are for macOS/Linux. On Windows, set the environment variables with $env:NAME = "value" in PowerShell. 3. Set up the project mkdir resilient-llm && cd resilient-llm npm init -y npm pkg set type=module npm install @anthropic-ai/sdk cockatiel npm install -D typescript @types/node type: module matters: Cockatiel 4 ships ESM only. Add a minimal tsconfig.json so your editor and tsc understand .ts imports: { "compilerOptions": { "target": "es2022", "module": "nodenext", "strict": true, "noEmit": true, "allowImportingTsExtensions": true, "skipLibCheck": true }, "include": "src" } 4. Define the retry and circuit-breaker policies Create src/resilient.ts . The first job is deciding which errors count as transient. The SDK throws typed subclasses of Anthropic.APIError with a status field, which makes this a one-liner: python import Anthropic from "@anthropic-ai/sdk"; import { BrokenCircuitError, ConsecutiveBreaker, ExponentialBackoff, circuitBreaker, fallback, handleWhen, retry, wrap, } from "cockatiel"; export const PRIMARY MODEL = process.env.PRIMARY MODEL ?? "claude-opus-5"; export const FALLBACK MODEL = process.env.FALLBACK MODEL ?? "claude-sonnet-5"; // maxRetries: 0 hands retries to Cockatiel. The SDK default of 2 would // multiply every Cockatiel attempt by three. const client = new Anthropic { maxRetries: 0, timeout: 60 000 } ; // Worth retrying: network errors, 408/409/429, and any 5xx 529 = overloaded . // 400/401/403/404 are our mistake or a dead model; retrying just burns time. const isTransient = err: unknown : boolean = err instanceof Anthropic.APIConnectionError || err instanceof Anthropic.APIError && err.status === 408 || err.status === 409 || err.status === 429 || err.status ?? 0 = 500 ; // 3 retries after the first call, starting around 500ms and capped at 8s, with jitter. export const retryPolicy = retry handleWhen isTransient , { maxAttempts: 3, backoff: new ExponentialBackoff { initialDelay: 500, maxDelay: 8 000 } , } ; // Open after 5 consecutive transient failures; let one probe through after 30s. export const breaker = circuitBreaker handleWhen isTransient , { halfOpenAfter: 30 000, breaker: new ConsecutiveBreaker 5 , } ; // Retry outside, breaker inside. An open circuit throws BrokenCircuitError, // which isTransient doesn't match, so the retry loop stops immediately. const primaryPolicy = wrap retryPolicy, breaker ; retryPolicy.onRetry { attempt, delay } = console.warn retry ${PRIMARY MODEL} failed; retry ${attempt}/3 in ${Math.round delay }ms , ; breaker.onBreak = console.warn breaker open: fast-failing ${PRIMARY MODEL} for 30s ; breaker.onHalfOpen = console.warn breaker half-open: probing ${PRIMARY MODEL} ; breaker.onReset = console.warn breaker closed: ${PRIMARY MODEL} healthy again ; Two details: maxAttempts counts retries after the initial call, so this policy makes up to four requests. And because the breaker sits inside the retry, every failed attempt feeds ConsecutiveBreaker ; one fully failed request is four of the five strikes. 5. Add the model fallback Append this to src/resilient.ts . The fallback policy is built per call because its factory can't see the wrapped function's arguments and needs the prompt. It holds no state, so that's free: // Fall back when the primary is unhealthy or gone, not when the request itself is bad. const shouldFallBack = err: unknown : boolean = isTransient err || err instanceof BrokenCircuitError || err instanceof Anthropic.NotFoundError; export interface Answer { model: string; text: string; } async function callModel model: string, prompt: string : Promise