●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Building a Budget Circuit Breaker for Claude API in Production — Auto-Halt When Daily Token Spend Exceeds Your Cap
A practical guide to enforcing daily and monthly Claude API budget caps in production. Includes copy-paste Cloudflare Workers + KV / Durable Objects code, three response strategies (halt, degrade, alert), and the operational habits that keep the breaker honest.
"Opened the Anthropic Console this morning and there was a giant token spike at 3 AM I can't explain — we burned three months of budget overnight." If you run Claude API in production for any length of time, you'll eventually have a story like that. I've had my own version: a nightly batch job lost its termination condition and looped through the same task list twice. By the time I noticed, the bill had moved past the point where "we'll fix it tomorrow" was acceptable.
The frustrating part is that there's no shortage of cost optimization advice — prompt caching, model downgrade, output truncation, all of which absolutely work. But almost none of that material talks about the other half of the problem: what happens when the bad scenario actually fires. Optimization lowers the average cost per request. It does nothing to bound the worst case. This article fills that gap. We'll build a circuit breaker that sits in front of every Claude API call, enforces daily and monthly token budgets, and decides — based on your business rules — whether to halt, degrade, or just notify when the cap is hit.
I run this pattern in production across the four Lab sites I operate. On the nights when scheduled jobs spike unexpectedly, the breaker quietly stops the runaway, and I find out from logs the next morning instead of from the billing dashboard the next week. The implementation isn't complicated, but the design choices are subtle, and that's what we'll spend the article on.
The first thing to untangle: cost optimization and budget enforcement are different problems. Conflating them puts you in the worst position — you've optimized but still get burned.
Speaking from experience, the moment right after a successful optimization push is when overconfidence creeps in. "We cut average per-call cost by 30% with Haiku routing — surely cost is under control now." But the average dropping makes spikes harder to see, not easier. You need both layers.
It's also worth being honest about what cost optimization buys you and what it doesn't. Optimization is essentially a discount on every transaction — the more transactions, the more dollars saved. Budget enforcement is a backstop against an entirely different failure mode, where the number of transactions explodes for reasons unrelated to product growth. The two are complementary, not substitutes. Teams I've talked to often have a sophisticated optimization story (caching ratios, model routing logic, eval pipelines for prompt efficiency) and effectively no enforcement story. That asymmetry is what catches you out.
If you've ever had a moment of "wait, why was the spend that high yesterday?" while looking at a billing dashboard, you've already lived the gap that this article is filling. The breaker doesn't make those moments impossible — it just means you find out before they become a billing surprise.
Every cost-optimization technique works by reducing the per-request unit cost. Prompt caching, downgrading from Sonnet to Haiku, tighter max_tokens, smarter stop_sequences — they all cut unit cost by some percentage. But if request count explodes by 10x, the bill still goes up by an order of magnitude.
There's also a measurement-versus-control distinction worth being clear about. Anthropic Console gives you excellent observability — you can see what was spent, by which model, when. But observability is not control. By the time you're looking at a console, the spend has already happened. The breaker is the layer that converts "we observed too much spend" into "we refused the spend before it happened." Without that conversion, your incident response is always reactive.
A second framing I find useful: the breaker is essentially an SLA between your service and your finance department. The SLA says "this service will never exceed $X per day, no matter what the upstream code does." Once you frame it that way, the design questions follow naturally. What window? What threshold? What does "never exceed" actually mean (hard cap vs soft cap)? Who has authority to raise the threshold? These are the same questions any service-level commitment requires, applied to cost instead of availability.
Budget incidents typically come from one of three sources:
Infinite-loop class: an agent misjudges its termination condition and calls the same tool over and over
Background-job class: a batch or scheduled task loses idempotency, and tomorrow's run reprocesses yesterday's data
Hostile-traffic class: a weakly-authenticated endpoint gets discovered and externally hammered
None of these are stopped by optimization. What you need is a mechanism that measures actual consumption and blocks the call itself when a threshold is crossed. That's the circuit breaker pattern, originally invented for microservice fault isolation but directly applicable to cost.
Three states of a circuit breaker, applied to Claude API
A physical circuit breaker has two states (closed/open). The software version has three:
Closed (normal): requests pass through; usage is recorded
Open (tripped): requests are rejected immediately with a fallback response
Half-Open (tentative): after a cool-down, a small number of test requests are allowed through to decide whether to reset
For Claude API budget management, "half-open" almost always becomes "wait for the daily or monthly window to roll over." Trying again 60 seconds after a budget overrun makes no sense. Resetting at midnight UTC (or your tenant's billing window) does.
In my own builds, I land on these specific decisions:
Measure both input and output tokens separately (because prices differ per direction and per model)
Two time windows: daily (resets at UTC 00:00) and monthly (resets on the 1st)
Check before the call, not after — a post-hoc check is too late under concurrency
That last point matters more than it sounds. If you record usage after each call and check on the next, a burst of concurrent calls all see "we still have budget" simultaneously and pass through. In high-concurrency production, only a pre-call check actually bounds spend.
✦
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
✦You can now prevent the surprise 'we just spent five times the monthly budget overnight' incident with a circuit breaker that requires almost no changes to your existing call sites
✦You'll learn how to enforce daily and monthly token caps using Cloudflare Workers + KV or Durable Objects, with copy-paste-ready production code that handles concurrency, streaming, and prompt caching correctly
✦You'll be able to choose among three real-world strategies — full halt, graceful degradation, alert-only — and apply the right one for the kind of service you operate, with implementations for each
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.
Minimum viable implementation — Cloudflare Workers + KV daily cap
Let's start with the smallest thing that works. This stores daily usage in Cloudflare KV and rejects calls past the cap. For traffic up to maybe 10,000 requests per day, this is genuinely sufficient.
// src/lib/budget-breaker.tsimport Anthropic from "@anthropic-ai/sdk";const DAILY_TOKEN_CAP = 5_000_000; // 5M tokens per day (example)interface Env { ANTHROPIC_API_KEY: string; BUDGET_KV: KVNamespace; // bound in wrangler.toml}interface BudgetState { inputTokens: number; outputTokens: number; date: string; // YYYY-MM-DD}// UTC-anchored date key (consistent across regions)function todayKey(): string { return new Date().toISOString().slice(0, 10);}async function readBudget(env: Env): Promise<BudgetState> { const key = `budget:${todayKey()}`; const raw = await env.BUDGET_KV.get(key); if (!raw) return { inputTokens: 0, outputTokens: 0, date: todayKey() }; return JSON.parse(raw) as BudgetState;}// Pre-flight check: returns whether budget remainsexport async function checkBudget(env: Env): Promise<{ allowed: boolean; used: number; remaining: number;}> { const state = await readBudget(env); const used = state.inputTokens + state.outputTokens; return { allowed: used < DAILY_TOKEN_CAP, used, remaining: Math.max(0, DAILY_TOKEN_CAP - used), };}// Post-call: record actual consumption (KV TTL auto-cleans after 48h)export async function recordUsage( env: Env, inputTokens: number, outputTokens: number): Promise<void> { const key = `budget:${todayKey()}`; const state = await readBudget(env); state.inputTokens += inputTokens; state.outputTokens += outputTokens; await env.BUDGET_KV.put(key, JSON.stringify(state), { expirationTtl: 60 * 60 * 48, });}// Wrapper: budget-aware Claude API callexport async function callClaudeWithBudget( env: Env, request: Anthropic.MessageCreateParams): Promise<Anthropic.Message | { error: "BUDGET_EXCEEDED" }> { const budget = await checkBudget(env); if (!budget.allowed) { console.warn(`[BUDGET] Blocked. used=${budget.used} cap=${DAILY_TOKEN_CAP}`); return { error: "BUDGET_EXCEEDED" }; } const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); const response = await client.messages.create(request); await recordUsage( env, response.usage.input_tokens, response.usage.output_tokens ); return response;}// Expected behavior:// Normal: returns Anthropic.Message with usage { input_tokens, output_tokens }// Once daily cap is hit: returns { error: "BUDGET_EXCEEDED" } until the next UTC midnight
The big advantage of this version is that it's hard to get wrong. You replace client.messages.create() with callClaudeWithBudget() at the call site, and your existing code stays put.
There's one important caveat. Cloudflare KV is eventually consistent — writes can take up to 60 seconds to propagate globally. In high-concurrency setups (above ~100 RPS), multiple requests will see "still has budget" at roughly the same time, all pass through, and you can overshoot the cap by 10–20%. For most services that's tolerable. When it isn't, you graduate to Durable Objects.
A second consideration with the KV approach is failure handling. What happens if the KV get call itself fails — Cloudflare network blip, KV namespace temporarily unavailable, anything? You have to choose an explicit policy: fail-open (let the call through) or fail-closed (block the call). Both are wrong in different ways. Fail-open means a KV outage temporarily disables your cost protection. Fail-closed means a KV outage takes down your service even though the actual upstream is healthy.
The pragmatic default I use is fail-open with a loud alert. The reasoning: KV outages are rare and short, and the cost of a few minutes' worth of unprotected calls is almost always less than the cost of a service outage. But fire an alert immediately so an engineer knows the breaker is degraded — that way, if a real cost incident happens during a KV outage, the team already knows the safety net is down and can take manual action.
The same logic applies to Durable Object failures, though DO failures within a single region are rarer than KV consistency lag.
Strict control under high concurrency — Durable Objects with atomic counters
When KV's eventual consistency stops being acceptable, Durable Objects (DO) is the next step. A DO instance is single-threaded and strongly consistent for a given ID, so funnelling all requests for one budget key through a single DO gives you atomic counter updates.
// src/durable-objects/budget-tracker.tsexport class BudgetTracker { private state: DurableObjectState; private cap: number = 5_000_000; constructor(state: DurableObjectState) { this.state = state; } async fetch(request: Request): Promise<Response> { const url = new URL(request.url); if (url.pathname === "/check") { const used = (await this.state.storage.get<number>("used")) ?? 0; return Response.json({ allowed: used < this.cap, used, remaining: Math.max(0, this.cap - used), }); } if (url.pathname === "/record") { const { tokens } = await request.json<{ tokens: number }>(); // CRITICAL: use storage.transaction() for atomic update await this.state.storage.transaction(async (txn) => { const current = (await txn.get<number>("used")) ?? 0; await txn.put("used", current + tokens); }); return Response.json({ ok: true }); } if (url.pathname === "/reset") { // Daily reset endpoint, called from a Cron Trigger await this.state.storage.deleteAll(); return Response.json({ ok: true }); } return new Response("Not Found", { status: 404 }); }}// Worker side: route all requests to a single DO per dayexport default { async fetch(request: Request, env: Env): Promise<Response> { const todayKey = new Date().toISOString().slice(0, 10); const id = env.BUDGET_DO.idFromName(`budget-${todayKey}`); const stub = env.BUDGET_DO.get(id); const checkRes = await stub.fetch("https://do/check"); const { allowed, used, remaining } = await checkRes.json<{ allowed: boolean; used: number; remaining: number; }>(); if (!allowed) { return new Response( JSON.stringify({ error: "BUDGET_EXCEEDED", used, remaining }), { status: 429, headers: { "content-type": "application/json" } } ); } const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages: [{ role: "user", content: "..." }], }); const totalTokens = response.usage.input_tokens + response.usage.output_tokens; await stub.fetch("https://do/record", { method: "POST", body: JSON.stringify({ tokens: totalTokens }), }); return Response.json(response); },} satisfies ExportedHandler<Env>;// Expected behavior:// - 100 concurrent calls all increment the counter exactly once// - The moment the cap is hit, subsequent requests get HTTP 429 with BUDGET_EXCEEDED// - Worst-case overshoot: one request's worth (a single in-flight call passing the check)
The trade-off is worth naming explicitly. KV gives you fast reads but loose write consistency. DO gives you strong consistency but a single-instance bottleneck. Below ~1,000 RPS, a single DO handles the load fine. Above that, you shard the DO by sub-window (e.g., one DO per hour) and sum across them in the Worker. For most small-to-mid SaaS workloads, "start with DO, shard later if needed" is the pragmatic path.
Three response strategies — halt, degrade, or alert
What does the breaker do when it trips? This is a business decision more than a technical one. I think of it as three distinct strategies.
Strategy A: Hard Halt
Return 429 BUDGET_EXCEEDED immediately and refuse the call. Maximum cost protection, worst user experience. Good for batch jobs, internal tools, and domains where overspend is more harmful than downtime — finance and healthcare often live here.
Strategy B: Graceful Degradation
Block the upstream call but return one of the following instead:
A cached template response ("we're temporarily under heavy load — please retry in N minutes")
An automatic fallback to a cheaper model (Sonnet 4.6 → Haiku 4.5)
A local approximation (summarize → take first N characters; sentiment → keyword match)
User experience degrades but doesn't break entirely. For most consumer-facing services, this is the default.
Strategy C: Alert-Only
Don't block. Just fire a Slack or email notification the moment the threshold is crossed. Suitable for early-stage services or trusted internal users where "miss the cap, but know about it" is the right balance.
A wrinkle worth mentioning on Strategy B: the fallback model itself consumes budget. If Sonnet hits its daily cap and you fall back to Haiku, you're now spending Haiku tokens. That's almost always cheaper, but it does mean your "Haiku budget" can run out too. The robust pattern is to give the fallback its own quota — perhaps 30% of the primary model's spend — and have a final tier (cached response) when even the fallback is exhausted. This nested fallback design is what makes the difference between a system that gracefully degrades and one that just delays the user-visible failure by a few minutes.
Another nuance for Strategy B: cached-response is most effective when paired with rate limiting. Without rate limits, a user retrying the cached "please wait" message in a tight loop can still drive a meaningful number of upstream calls (depending on where you place the cache). Place the cached response before the breaker check rather than after, and gate it with a per-user retry-after header so clients understand the recovery window. Browsers and well-behaved client libraries will respect that header automatically.
For Strategy C, the alert channel matters more than the alert text. Slack is fine for office-hours, but for a 24/7 service, you want at least one alert path that wakes someone up — PagerDuty, Opsgenie, or even SMS via Twilio. The cheapest way to lose a budget battle is to have an alert sitting in a Slack channel no one reads at 3 AM.
In practice, Strategy B is the most useful default. Here's the implementation:
// src/lib/budget-breaker-degraded.tstype DegradationStrategy = "halt" | "fallback-model" | "cached-response" | "alert-only";interface BreakerConfig { cap: number; strategy: DegradationStrategy; fallbackModel?: string; cachedResponse?: string; webhookUrl?: string; // Slack incoming webhook}export async function callClaudeWithDegradation( env: Env, config: BreakerConfig, request: Anthropic.MessageCreateParams): Promise<Anthropic.Message> { const stub = getBudgetStub(env); const checkRes = await stub.fetch("https://do/check"); const { allowed, used } = await checkRes.json<{ allowed: boolean; used: number }>(); // Pre-warning at 80% (return value not affected) if (used >= config.cap * 0.8 && config.webhookUrl) { fetch(config.webhookUrl, { method: "POST", body: JSON.stringify({ text: `Claude API budget at ${Math.round((used / config.cap) * 100)}% (used=${used} / cap=${config.cap})`, }), }).catch(() => {}); // notification failures are non-fatal } if (allowed) { return await callClaudeAndRecord(env, request); } // Branch on strategy when over budget switch (config.strategy) { case "halt": throw new BudgetExceededError("Daily budget exhausted"); case "fallback-model": // Retry with a cheaper model (give the fallback its own quota for full robustness) return await callClaudeAndRecord(env, { ...request, model: config.fallbackModel ?? "claude-haiku-4-5-20251001", }); case "cached-response": return makeFakeMessage( config.cachedResponse ?? "We're under temporary load. Please retry shortly." ); case "alert-only": await notify(config.webhookUrl, `[BREAKER] cap exceeded but allowing through. used=${used}`); return await callClaudeAndRecord(env, request); }}class BudgetExceededError extends Error {}function makeFakeMessage(text: string): Anthropic.Message { return { id: `msg_fallback_${Date.now()}`, type: "message", role: "assistant", content: [{ type: "text", text, citations: null }], model: "fallback", stop_reason: "end_turn", stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: null, cache_read_input_tokens: null }, } as Anthropic.Message;}// Expected outputs:// Normal: full Anthropic.Message// Over budget + fallback-model: Anthropic.Message with model = Haiku// Over budget + cached-response: { id: "msg_fallback_...", model: "fallback", usage: { ..._tokens: 0 } }
A simple decision rule for choosing among them:
Is it acceptable for users to wait? → Yes: Strategy B with cached-response
Is it acceptable for output quality to degrade? → Yes: Strategy B with fallback-model
Are users internal-only and is missing the cap survivable? → Yes: Strategy C
None of the above → Strategy A
Common pitfalls
The implementations above will run, but a few traps lie in wait. Most of these I've stepped on personally.
Pitfall 1: timezone drift across the day boundary
I wrote "daily reset" above, but new Date().toISOString().slice(0, 10) returns a UTC date. If your operational mental model is "midnight in our timezone," you'll be surprised. For Tokyo-based ops, the reset effectively happens at 09:00 JST, which can mean nightly batch jobs straddle two budget windows and double-count.
The fix is to be explicit. If you want a Tokyo-based day, format your key with Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Tokyo" }) (en-CA gives you ISO-like YYYY-MM-DD).
Pitfall 2: missing the usage field in streaming mode
The non-streaming client.messages.create() returns usage directly on the response. In streaming mode (stream: true), usage arrives in the final message_delta event. Miss that handler and your counter stays at zero forever — meaning the breaker is silently disabled.
// Token recording for streaming responseslet inputTokens = 0;let outputTokens = 0;for await (const event of stream) { if (event.type === "message_start") { inputTokens = event.message.usage.input_tokens; } if (event.type === "message_delta") { outputTokens = event.usage.output_tokens; } yield event; // forward to the client}// Always record after the stream — wrap in try/finally to handle disconnectsawait recordUsage(env, inputTokens, outputTokens);// Expected behavior: recording fires on normal completion AND on client disconnects /// timeouts, by placing recordUsage() inside a finally block in the real implementation.
Pitfall 3: double-counting cached prompt tokens
When prompt caching is enabled, usage includes cache_read_input_tokens and cache_creation_input_tokens. These have different unit costs from regular input tokens (cache reads are ~10% of input price, cache writes ~125%). If you simply sum all token fields, your "tokens used" number diverges from your actual dollar spend.
For a budget that tracks money, weight each field properly:
// Per-million pricing (USD), Sonnet 4.6 example as of 2026-04const PRICING = { inputPer1M: 3.0, outputPer1M: 15.0, cacheWritePer1M: 3.75, // 1.25x of input cacheReadPer1M: 0.3, // 0.1x of input};function estimateCostUsd(usage: Anthropic.Usage): number { const input = (usage.input_tokens * PRICING.inputPer1M) / 1_000_000; const output = (usage.output_tokens * PRICING.outputPer1M) / 1_000_000; const cacheW = ((usage.cache_creation_input_tokens ?? 0) * PRICING.cacheWritePer1M) / 1_000_000; const cacheR = ((usage.cache_read_input_tokens ?? 0) * PRICING.cacheReadPer1M) / 1_000_000; return input + output + cacheW + cacheR;}// Pricing changes — always pull current numbers from Anthropic's official pricing page.
A money-denominated budget ("up to $50 per day") communicates better to non-engineers than a token-denominated one. It's also more accurate when caching is heavily used.
Pitfall 4: thinking of the breaker as binary, forgetting the operational mode flag
Right after deployment, a breaker can incorrectly stop legitimate traffic if your threshold turned out to be too low. Run it in monitor mode (logs only, doesn't block) for the first few days, validate the threshold against real traffic, then flip to enforce mode. Make this a config flag, not a code change.
const BREAKER_MODE: "monitor" | "enforce" = (env.BREAKER_MODE as any) ?? "monitor";if (!allowed) { if (BREAKER_MODE === "enforce") { return { error: "BUDGET_EXCEEDED" }; } else { console.warn(`[BREAKER] Would block (monitor mode). used=${used}`); // pass through normally }}
Pitfall 5: forgetting to bound concurrency at the source
A breaker bounds total spend over a time window, but it doesn't bound peak concurrency. If 10,000 simultaneous requests all arrive in the same second, every one of them sees the budget as available, every one passes the check, and you've effectively burst through your cap by 10,000x for a few hundred milliseconds. The breaker will recover quickly — the next second's requests are blocked — but the damage is done.
The mitigation is a concurrency limiter upstream of the breaker. Cloudflare Workers Queues, an in-memory semaphore on a single Worker, or an external rate-limiter like Upstash all work. The principle is the same: bound how many in-flight Claude calls can exist at any moment, regardless of how many users are trying. A breaker plus a concurrency limit gives you defense in depth — the breaker handles cumulative spend, the limiter handles instantaneous spend.
Pitfall 6: not having a manual override path
Sometimes the breaker is doing exactly what it was designed to do, and you still want it off. A demo for an investor, a critical customer escalation, a postmortem that needs the breaker temporarily lifted to reproduce an issue. Without a clean override mechanism, the team's only option is "edit the code and redeploy," which is slow at best and dangerous under stress. Bake in an override flag that can be set per scope and time-limited automatically (e.g., "override active for 60 minutes, then auto-revert"). Log all overrides loudly so they're impossible to forget about.
Three operational habits that keep the breaker honest
The implementation matters less than the operational practice. A few habits I've found make the difference between a breaker that protects you and one that becomes a forgotten box.
First, set the threshold from the P95 of the last 30 days, not from a number that felt right at design time. Static thresholds don't scale with growth. Pull the daily consumption from Anthropic Console or your own analytics, take the P95 (or mean × 1.5 as a rougher proxy), and use that as your starting threshold. Revisit monthly.
Second, send a monthly "remaining-budget report" to your team. At month end, share what percentage of budget was consumed, how many times the breaker tripped, and which endpoints / time windows dominated usage. Without this reporting cadence, the breaker becomes an opaque box and people stop trusting it. Loss of trust kills the discipline that makes the breaker effective.
Third, test the breaker itself with E2E tests. Use mock KV / DO in CI to verify behavior at three points: 99% of cap, exactly at cap, and 101% of cap. The worst incident is a real overrun plus a buggy breaker — finding out simultaneously that your cost protection didn't work. This is exactly the kind of thing worth investing in test coverage for.
Fourth, treat each trip as a learning event, not just a status to clear. The wrong instinct after a trip is to immediately raise the threshold and move on. The right one is to investigate: was it an infinite loop, healthy growth, or hostile traffic? Raise the threshold only after confirming healthy growth. I've made the opposite mistake — repeatedly lifting the cap during rapid growth — and ended up unable to distinguish bug-driven runaway from real product success.
Fifth, make breaker controls touchable by the broader team, not just one engineer. Threshold and strategy should be controllable via Wrangler secrets or environment flags, with the procedure documented in a runbook. When a 3 AM incident requires a temporary cap raise and the on-call engineer is asleep, having two or three people who can safely operate the breaker turns hours of downtime into minutes.
A more subtle habit, and one I've come around to over time: treat the threshold as a forecast, not a budget. The number you set isn't "what we want to spend" — it's "what we expect to spend, plus a margin for healthy variance." Setting the cap at exactly your monthly budget guarantees you'll trip on any month with normal-but-elevated usage. Setting it 30–50% higher than the typical month gives you protection against the actual disaster scenarios (10x runaway) without false-positive trips during merely busy periods.
This shifts the framing usefully. The conversation with the business stops being "we set the cap at $X, why are we tripping?" and becomes "we set the cap at the 99th percentile of expected daily spend, and trips correspond to genuine anomalies that warrant attention." The breaker becomes a signal of unusual activity rather than a routine operational nuisance.
Dashboards and observability — making the breaker visible
Implementing a breaker is half the job. Visualizing its state is the other half. A breaker that's silently broken is worse than no breaker at all, because it gives false confidence.
The minimum dashboard I keep:
Cumulative tokens for the current day (line chart, midnight to now)
Daily token consumption for the last 30 days (bar chart with the cap drawn as a red line)
Breaker trips (counters for today / week / month)
Breakdown by model and by endpoint (pie or stacked bar)
Cloudflare Workers Analytics Engine is the lowest-friction option here. One env.ANALYTICS.writeDataPoint() call per Claude request and you can run SQL-like queries afterwards. For Datadog or Grafana Cloud, push from Workers to an HTTP endpoint on a schedule.
// Recording a data point for Workers Analytics Engineenv.ANALYTICS.writeDataPoint({ blobs: [ request.model ?? "unknown", // blob1: model config.strategy, // blob2: strategy allowed ? "passed" : "blocked", // blob3: outcome ], doubles: [ response.usage?.input_tokens ?? 0, // double1: input tokens response.usage?.output_tokens ?? 0, // double2: output tokens estimateCostUsd(response.usage), // double3: estimated USD ], indexes: [todayKey()],});// Sample query (Wrangler / GraphQL API):// SELECT blob1 AS model, SUM(double1+double2) AS tokens, SUM(double3) AS cost_usd// FROM claude_api_dataset// WHERE timestamp > NOW() - INTERVAL '24' HOUR// GROUP BY blob1 ORDER BY tokens DESC;
Crucially, record blocked requests too. The "blocked count" isn't just a failure metric — it's an early signal of mismatch between your budget setting and your service's growth trajectory.
Designing for multiple models and multiple teams
As a service grows, you'll want separate budgets per model (Sonnet vs Haiku) or per use case (frontend chat vs backend batch). Retrofitting that later means scattering budget checks throughout the code. Building it in from the start is much cleaner.
The cleanest pattern is to introduce a scope argument and key the DO by scope:
Scoped budgets prevent one runaway feature from starving the rest of the service. If you also want to enforce a global ceiling, run two checks in parallel — one against the scope, one against a "global" scope that aggregates all calls.
A practical observation: too many scopes leads to the situation where you're rejecting calls in one scope while another scope has plenty of headroom. Three to five scopes is roughly the sweet spot in my experience — granular enough to isolate failures, coarse enough that you're not perpetually rebalancing budgets.
When you do split into scopes, the question of "global cap vs sum of scope caps" comes up. There are two valid approaches:
Sum-of-scopes: each scope has its own cap, and the global ceiling is implicitly the sum. Simple to reason about, but you can't redistribute headroom. If internal-tools is at 5% and frontend-chat is at 95%, the system can't borrow from internal-tools to keep frontend traffic flowing.
Global ceiling + scope quotas: each scope has a soft cap, but the hard limit is global. This allows rebalancing under load — frontend can borrow from internal-tools when needed — at the cost of more complex reasoning about who's responsible for what.
I default to sum-of-scopes for clarity. Most teams want to attribute costs to specific features, and that's only possible if scope budgets are independent. The rebalancing flexibility of a global ceiling sounds nice but in practice creates ambiguous accountability ("frontend went over budget, but internal-tools was under, so the global stayed fine — whose fault is the overrun?"). For services that genuinely need rebalancing, I'd consider a single global cap and skip per-scope budgets entirely.
Closing — the one thing to do today
You don't need the full system on day one. Before you log off today, write the alert-only version for one endpoint. Twenty lines of code that fire a Slack notification when today's running total crosses 80% of your intended cap. That alone shifts you from "found out about the overrun a week later" to "got pinged before it became a real overrun."
Once that's in place and you know your alert noise level, layer in the actual blocking logic. By the time you turn enforcement on, you'll already know whether your threshold is too tight or too loose, which is exactly the information that makes the difference between a breaker that protects you and one that breaks production.
A breaker isn't a tool to minimize cost. It's a tool to keep cost from doing something you didn't intend. The bigger your Claude API footprint gets, the larger the return on that small investment.
One closing thought on the operational side. Putting a circuit breaker in your service means accepting that, on some bad day, your code will deliberately reject user traffic. That's a meaningful business decision — it shouldn't be made by engineers alone. Agree with the business stakeholders ahead of time on which strategy to use, what threshold to set, and who decides when the breaker trips. The technical implementation is the easy part. The agreement takes more time but pays off the moment a real incident actually happens.
One more thought specific to small teams and indie developers, since this is the audience I'm closest to. Cost incidents at small scale don't bankrupt a service, but they do erode confidence — yours, and your team's — in shipping new AI features. After the second or third unexpected billing spike, the temptation is to either over-engineer the cost story (reducing every feature to a Haiku call with a 200-token max) or stop shipping AI features altogether. A simple breaker is the middle path. It lets you keep moving fast, knowing that the worst case is bounded. That psychological effect — "if this experiment goes wrong, the breaker catches it" — turns out to be one of the more valuable outcomes, beyond the direct cost protection.
In smaller teams without a dedicated platform engineer, the breaker often gets built once and never revisited. That's fine if the threshold was set conservatively from the start, but it does mean the system slowly drifts out of sync with actual traffic. A quarterly threshold review — even a 15-minute conversation — is enough to keep the breaker meaningful as the service evolves.
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.