●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
Designing Graceful Degradation for the Claude API — A Four-Tier Fallback Architecture That Keeps AI Features Quietly Alive
Once Claude API features hit real production traffic, model-level fallback alone stops being enough. This article walks through an SLI-driven four-tier degradation design, with Python and TypeScript code, SLO burn-rate alerting, and the operational trade-offs an indie developer actually runs into.
After Claude API features have been running in production for a few months, you start running into a class of incidents that model-level fallback cannot fix. Sonnet and Haiku both still respond, but p99 latency is glued to four seconds. Page load feels twice as slow to users, while error rates stay well below the circuit breaker threshold. From the system's point of view, everything looks healthy. From the user's point of view, quality has quietly collapsed. Coping with this requires a coarser unit than per-model substitution: an explicit, tiered degradation of the service itself.
I am Masaki Hirokawa, an indie developer running the Dolice app business since 2014. The portfolio has crossed 50 million cumulative downloads, and AdMob revenue has matured to a level where any minutes of downtime translate directly into lost income. Once Claude API features were embedded into that stack, I had to decide explicitly which things I was willing to give up during incidents and which things I absolutely had to keep alive. This article is about how I encoded that contract into four tiers that switch automatically.
Why model-level fallback is not enough
Many availability articles stop at "Sonnet falls back to Opus, Opus falls back to Haiku." That chain is useful, but it only addresses one face of production failure. Several others look like this:
Models respond, but the SLO burn rate is spiking: Latency budget is being burned five times faster than the monthly target allows. Circuit breakers stay closed, so the system reports itself as healthy, but the customer experience is silently degrading.
Budget pressure from above: Monthly Anthropic spend is on track for 160 percent of plan. The API still works, but if you keep paying full price for full quality, you will not make it to month-end. You want to demote cost-heavy features first.
Per-tenant noisy neighbors: Overall metrics look fine, but one tenant is consuming an outsized fraction of the token budget. You need a knob that demotes features per tenant, not globally.
None of these are model-substitution problems. They are "how do I make this feature simpler" problems. Graceful degradation is the design vocabulary for that granularity.
The four-tier model
Across Dolice's app portfolio and the four Lab sites (Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab), I have standardized on the following four tiers. Every Claude-API-backed feature is mapped to one of them ahead of time.
Tier
Name
Behavior
Cost
Latency target
T0
Full Quality
Sonnet 4 / Opus 4 with full reasoning
100%
p99 1.5s
T1
Reduced Quality
Haiku 4.5, extended thinking off
30–40%
p99 600ms
T2
Cached Response
Semantic cache hit, or pre-computed template
5–10%
p99 80ms
T3
Static Fallback
Predefined static UX, no AI call at all
0%
p99 30ms
The crucial point is that T2 and T3 do not call the API at all. Many resilience designs stop at T0–T1, but the real safety net during a peak-hour incident is in T2 and T3. For example, a "description of today's recommended wallpaper" feature can collapse to a plain static line at T3 with almost no perceived loss. A conversational search box, on the other hand, stays usable about 80 percent of the time once T2 serves cached answers.
✦
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
✦A tier-decision engine implemented in both Python and TypeScript that auto-switches across p99 latency 800ms, 5xx rate 2%, and 429 rate 5% thresholds
✦Concrete rules for separating fail-closed and fail-open features, anchored in real numbers from a wallpaper app handling 1M+ daily requests and 50M cumulative downloads since 2014
✦A GitHub Actions chaos-drill workflow plus a 14.4x SLO burn-rate trigger that runs weekly against Cloudflare Workers preview environments
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.
To switch tiers mechanically, you need to decide ahead of time what to measure. I aggregate the following three Service Level Indicators over a 1-minute window and feed them into a small decision engine:
p99 latency: 99th percentile latency of Claude API calls in the last minute. Threshold 800 ms triggers a candidate step-down.
5xx rate: Share of 5xx responses in the same window. Threshold 2 percent triggers an immediate step-down.
429 rate: Share of 429 responses in the same window. Threshold 5 percent triggers a candidate step-down. If 429 and 5xx are both elevated, step down by two tiers at once.
These signals should not be read independently. Bundle them into an SLO burn rate. A 14.4x burn rate over a one-hour window means you are burning an hour of monthly budget in real time and should demote immediately. This adapts the Google SRE Book burn-rate philosophy to AI features, and it has aligned well with what I have actually seen in production.
Decision engine in Python
What this code solves: given the latest 1-minute SLI snapshot, it returns the tier to operate in next and an audit-friendly reason string. The function is intentionally pure so the same logic can run inside a Cloudflare Workers Durable Object, an AWS Lambda fronted by DynamoDB Streams, or a plain cron job.
# tier_decider.pyfrom dataclasses import dataclassfrom enum import IntEnumfrom typing import Literalclass Tier(IntEnum): T0_FULL = 0 T1_REDUCED = 1 T2_CACHED = 2 T3_STATIC = 3@dataclass(frozen=True)class SLISnapshot: """Last 1-minute SLI snapshot. Fed by Prometheus or CloudWatch upstream.""" p99_latency_ms: float err_5xx_rate: float err_429_rate: float burn_rate_1h: float # ratio against monthly SLO targetdef decide_tier( current: Tier, sli: SLISnapshot, feature_priority: Literal["critical", "standard", "optional"] = "standard",) -> tuple[Tier, str]: """Return the next tier and a human-readable reason.""" # 1. SLO burn rate over 14.4x -> jump to T2 or T3 if sli.burn_rate_1h >= 14.4: if feature_priority == "optional": return Tier.T3_STATIC, f"burn_rate={sli.burn_rate_1h:.1f} optional -> T3" return Tier.T2_CACHED, f"burn_rate={sli.burn_rate_1h:.1f} -> T2" # 2. 5xx rate above 2% -> step down once if sli.err_5xx_rate >= 0.02: return _step_down(current, reason=f"5xx={sli.err_5xx_rate:.1%}") # 3. 429 rate above 5% -> step down once if sli.err_429_rate >= 0.05: return _step_down(current, reason=f"429={sli.err_429_rate:.1%}") # 4. p99 latency above 800 ms -> step down once if sli.p99_latency_ms >= 800.0: return _step_down(current, reason=f"p99={sli.p99_latency_ms:.0f}ms") # 5. All signals healthy -> step up by one (the caller handles 30s hysteresis) if ( sli.p99_latency_ms < 500.0 and sli.err_5xx_rate < 0.005 and sli.err_429_rate < 0.01 and sli.burn_rate_1h < 2.0 ): return _step_up(current, reason="all SLIs healthy") return current, "no change"def _step_down(current: Tier, reason: str) -> tuple[Tier, str]: next_tier = Tier(min(current.value + 1, Tier.T3_STATIC.value)) return next_tier, f"step_down to {next_tier.name}: {reason}"def _step_up(current: Tier, reason: str) -> tuple[Tier, str]: next_tier = Tier(max(current.value - 1, Tier.T0_FULL.value)) return next_tier, f"step_up to {next_tier.name}: {reason}"
The reason this engine moves only one tier at a time is empirical. If recovery jumps you from T3 directly back to T0, the moment the underlying issue twitches you slam into the same wall again. The caller in my deployment waits 30 seconds of continuous healthy readings before stepping up.
TypeScript wrapper for the call site
The decision engine produces a tier number. The client wrapper turns that number into actual behavior on each request. The version I run on Cloudflare Workers as a BFF looks like this:
The piece of this design that survives contact with real traffic is "T2 means cache-or-regenerate-then-cache," not "T2 means error on miss." If T2 starts cold and you reject misses, every user is treated like an outage. Regenerating once at the cheaper tier and writing back gives T2 a graceful warm-up curve that matches what humans actually perceive.
Fail-closed versus fail-open as a per-feature decision
When you finally reach T3, you have to decide whether to fail-closed (refuse the feature outright) or fail-open (serve a simple static response and continue). My working rules:
Fail-closed for write-to-user-data features: auto-summarizing chat history, editing AI-managed lists, generating post-purchase confirmation text. A wrong answer here can corrupt the user's state. Refuse explicitly.
Fail-open for display-only features: related-link blurbs, conversational search summaries, today's recommendation copy. A flat static line is acceptable because the broader UX still works.
The wallpaper apps in the Dolice portfolio see roughly one million requests per day. With AdMob revenue tied directly to availability, a blank screen is the option of last resort. Most feature failures degrade quietly into static lines that users do not even notice. Over twelve years there have been only a handful of explicit complaints. Write-to-user-data features, by contrast, fail-closed without apology: "this feature is temporarily unavailable, please try again later" is much safer than a wrong write.
Putting chaos drills into CI
A degradation design is only trustworthy if you exercise it on a schedule. I run a weekly chaos drill in a Cloudflare Workers preview environment from GitHub Actions:
The first time I ran this drill, it surfaced a silly bug: the code had stepped down to T2, but the tier_used metric stuck at the old value for over thirty seconds because the Durable Object write had not propagated to the dashboard. Without the drill, I would have discovered that for the first time during a real production incident.
Five operational gotchas
Tier flap without hysteresis: instant step-up after recovery causes T0 to T2 oscillation every minute. Requiring 30 seconds of continuous healthy readings before stepping up eliminates the flap.
Cache TTL too short collapses T2: a 60-second TTL pushes T2 into mostly-miss territory under load. I use 10 minutes for hot keys and 60 minutes for cold per-user keys.
Static fallback text needs to be respectable: "data fetch failed" reads like a bug. "Today's recommendation is loading, please try again shortly" preserves trust during T3.
Alert on the tier transition itself: alerting on T1, T2, T3 entries individually is more intuitive than alerting on raw SLI thresholds. I use emoji per tier in Slack so the on-call (me) can read the timeline at a glance.
Tie cost telemetry to tier: plotting time spent in T1 against AdMob revenue over a week tells you which features can tolerate T2 for how long. In my operation this analysis ended up informing how I priced the Premium plan.
Why this design fits indie development
Closing thoughts on why the four-tier model is a particularly good fit for solo operators. I run the four Lab sites, the wallpaper app business, and my contemporary art practice in parallel as one person, so this perspective is real.
During incidents, there is no one to page. The value of a system that quietly demotes itself instead of failing loudly is therefore disproportionately high.
AdMob revenue varies sharply by feature. The four-tier map lines up directly with "what must not stop" versus "what we are willing to lose." Indie economics make the conversation simpler, not harder.
Predictable monthly spend matters when you are personally on the hook for it. SLO burn-rate-driven demotion is a safety valve that prevents a bad week from blowing up the AI budget.
The reason the Dolice app business has survived twelve years and 50 million cumulative downloads is not flashy features. It is the quiet accumulation of designs like this one, where failure stays small and recovery happens without anyone watching. Bringing the Claude API into that stack has not changed the principle, only the surface area to which it applies. I hope this design serves anyone else operating an AI feature alone.
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.