I shipped cl — a multi-account load balancer for Claude Code agent sessions — and it immediately started making a mistake I’d been making myself for two weeks. It was treating every HTTP 402 as “out of credits” and rotating the session to a different account.
The rotation was wrong most of the time. It took three separate incidents before I finally stopped trusting the status code.
Here’s what was happening. I’d fire off an agent session on account A. The Anthropic API would return 402 Payment Required. My balancer would see that code, mark account A as drained, and route the session to account B — context switch, new account, session reload. But fifteen minutes later, account A would be healthy again. Same request, it just needed thirty seconds.
The trap is specific. Anthropic returns 402 for two situations that call for opposite responses. One is genuine billing exhaustion — the response body says “insufficient credits” or “quota exceeded” — and the right call really is to rotate the session to an account with available headroom (remaining quota before the rate limit kicks in). But 402 also comes back for a periodic rate limit — a temporary throttle that clears on its own. The body in that case says something like “try again in 30 seconds.” Rotating on that kind of 402 is exactly wrong: you throw away a perfectly good account for one that might be closer to its own limit, you burn a context switch reloading the session, and in half a minute the original account would’ve been fine. I was treating every 402 as a billing failure.
The mirror trap is 5xx. A 500 is usually a transient server hiccup — retry it and it goes through. But some 5xx responses are deterministic: the body says unsupported_parameter. That request will fail identically every time you send it. My balancer was politely retrying those, burning API calls in a tight loop against a request the service would never accept.
The fix for both traps is the same: read the body before you decide what to do. The status code is a bucket — the body is the actual reason. In a multi-account balancer, this distinction is load-bearing. Wrong classification means a wasted rotation or an infinite retry loop, not just a single failed call.
Three rules the balancer now follows:
- A 402 is not a rotation signal until the body confirms it’s a billing error. If the body contains a retry phrase (“try again,” “retry after”), it’s a rate limit — back off, wait, and stay on the same account.
- Not every 5xx is retryable. If the body names a bad parameter, fail fast and surface the error rather than looping.
- Match deterministic-rejection patterns before retry-eligible ones. Error strings overlap in the wild —
max_tokenscan appear in both a “malformed request” error and a “context too long” error. If you test the retryable pattern first, the permanent failure sneaks through as transient. You loop forever on a request the API will never accept.
the ordered classifier give me the detail
The implementation that avoids the overlap trap is a short ordered table: (pattern, action) pairs where deterministic-rejection rules come first and you return on the first hit. The balancer uses this to decide between ROTATE, BACKOFF, RETRY, and FAIL before touching any routing logic.
import re
RULES = [
# deterministic — never retry, even on a 5xx
(r"unsupported_parameter|invalid_request|not supported", "FAIL"),
# rate limits — back off, stay on this account
(r"rate.?limit|try again|retry.?after|temporarily", "BACKOFF"),
# genuine billing — now it's safe to rotate to another account
(r"insufficient|quota exceeded|out of credits", "ROTATE"),
]
def classify(status: int, body: str) -> str:
text = (body or "").lower()
for pattern, action in RULES: # order matters: FAIL must beat BACKOFF
if re.search(pattern, text):
return action
return "RETRY" if 500 <= status < 600 else "FAIL"Order is load-bearing here. Because max_tokens appears in both a malformed-request body and a context-overflow body, the FAIL rule must win the tie — otherwise context-overflow looks retryable and you loop forever.
One practical note: don’t write these patterns from the documentation. Capture the actual bodies your provider sends with curl -i during real errors, pin a few as fixtures, and assert against them. Every vendor words these differently, and the strings are all you’ve got.
This applies whether you’re routing agent sessions across Anthropic accounts or just calling any third-party API: a status code tells you the bucket, not the decision. Two errors in the same bucket can need opposite responses — one wants a patient wait, another an immediate stop, and a third a full context rotation to a different account. The body is where the difference lives. Parse it before you route.