Over about 36 hours spanning July 29 to 31, Claude's serving infrastructure had four distinct incidents. Not one outage — four, each a different shape, each hitting a different scope.
Per independent status tracking: a 40-minute drop across all models the evening of the 29th, a four-hour-plus window of elevated errors the next morning, a 40-minute "degraded performance" period on Claude Opus 4.8 that afternoon, and a shorter 9-minute degraded-performance blip on Claude Sonnet 5 the morning of the 31st. Anthropic's own statement named the mechanism for the first two: multiple separate network failures cut into serving capacity, and the reroute response itself caused some requests to fail.
That last detail is the one worth building around. The fix caused failures of its own. If your circuit breaker only knows how to detect "down," this entire window looks like something else: flaky.
Two failure shapes, two very different bills
Hard failure. The provider returns a fast, clean error: connection refused, a 503, a timeout. Every reasonable retry library detects this on the first or second attempt and either backs off hard or opens a circuit. This is the failure mode most agent frameworks are built around, and it's genuinely the easy case.
Capacity-shaped failure. This is what "elevated errors" and "reduced availability" actually mean in practice: some calls succeed, some time out, some fail, mixed together over hours, because the provider is serving from reduced capacity rather than serving nothing. A naive consecutive-failure breaker was never designed to catch this:
typescript
// Trips only on N consecutive failures.
// A capacity-shaped incident rarely produces
// five failures in a row — it produces five failures
// spread across forty successful calls.
class NaiveBreaker {
private consecutiveFailures = 0;
private readonly threshold = 5;
recordResult(ok: boolean) {
this.consecutiveFailures = ok ? 0 : this.consecutiveFailures + 1;
}
isOpen() {
return this.consecutiveFailures >= this.threshold;
}
}
Every interspersed success resets the counter. The breaker never opens. Your retry logic keeps sampling a flaky distribution for four hours, and every sample is a billed call.
The fix is to stop counting streaks and start tracking a rate over a window:
typescript
// Trips on error RATE over a rolling window,
// regardless of whether failures are consecutive.
class RollingRateBreaker {
private window: boolean[] = []; // true = success
private readonly windowSize = 20;
private readonly errorRateThreshold = 0.3;
recordResult(ok: boolean) {
this.window.push(ok);
if (this.window.length > this.windowSize) this.window.shift();
}
isOpen() {
if (this.window.length < this.windowSize) return false;
const errorRate = this.window.filter((r) => !r).length / this.window.length;
return errorRate >= this.errorRateThreshold;
}
}
Twenty calls, six or more failures anywhere in the window, and it trips, whether those failures are clustered or scattered. That matches how capacity-shaped incidents actually behave.
The bill waiting on the other side of recovery
Here's the second cost, and it only shows up if your agent has a fallback provider configured. Say your harness fell back to a second model mid-incident. The task keeps running there. Then the primary recovers, and if nothing tracked which provider actually finished the job, you risk redoing the same work on both — paying twice for one logical task.
This isn't hypothetical. It's specific enough that incident write-ups from this week explicitly warned people not to duplicate work across harnesses once the primary provider came back.
The fix is a small reservation ledger keyed on the logical task, not the provider:
typescript
interface TaskAttempt {
taskId: string;
provider: string;
status: 'in_flight' | 'completed';
}
const attempts = new Map();
function beforeCall(taskId: string, provider: string) {
const existing = attempts.get(taskId) ?? [];
const doneElsewhere = existing.find(
(a) => a.status === 'completed' && a.provider !== provider
);
if (doneElsewhere) {
throw new Error(
Task ${taskId} already completed on ${doneElsewhere.provider}
);
}
existing.push({ taskId, provider, status: 'in_flight' });
attempts.set(taskId, existing);
}
One map. One check before the call goes out. It's the difference between "the outage cost us four hours of degraded service" and "the outage cost us four hours of degraded service, plus a duplicate invoice from the fallback provider we forgot was still running."
What to actually change this week
Replace consecutive-failure breakers with rolling-rate breakers, at least for provider calls.
If you run a fallback provider, key task completion by task ID, not by provider, and check it before every call, not just the first one.
Treat "degraded performance" status entries as a distinct alert category from "down." They're not lower priority. They're the ones your retry logic is least equipped to handle.
None of this prevents a network failure. It's the layer that decides what a network failure costs you afterward.
This is the exact class of failure AI CostGuard checks for before a call executes: not whether the provider is up, but whether the call about to go out is one your session has already paid for somewhere else.