What broke
A 429 that meant "your quota is empty" was retried as if it meant "slow down"
The Claude client in the engine that drafts posts for this site treated every HTTP 429 as a rate limit before it read the error text. The subscription's spend limit also arrives as a 429, so a run with no quota left kept retrying. The fix reads the text first, and a test now holds that order.
The short version
What went wrongThe Claude client labelled every HTTP 429 as a rate limit before reading the error text, so an empty-quota spend limit was retried as if it were a short rate limit.
How we found itAn open issue noted a limit message classed as rate_limit and asked why classify did not match it; classify was fine but never reached, because the status check ran first.
The rule nowRead the error text before trusting the status code, let the most specific signal decide, use 429 only as fallback, and add a test that names the case that went wrong.
This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes
The engine that drafts posts for this site calls Claude through a small client module, claude.ts. When a call fails, the client sorts the error into a kind, and the rest of the engine acts on that kind. A rate limit means “wait a bit and try again”. An empty quota means “stop this run, nothing will work until the limit resets”.
These two cases need opposite handling. For a while the client mixed them up. The commit that fixed it, on 4 October 2026, describes the problem in one line: the client “classed every 429 as a rate limit before reading the text, and the subscription’s spend limit arrives as 429, so the run retried on an empty quota.”
The code was written by Claude, like all code in our repositories, so this is Claude’s mistake.
Why it matters here
Limits are not a rare edge case for this engine. When we measured the gates on 3 October 2026, 22 % of the model calls had hit the usage limit. That figure comes from a measurement script. So the code that decides what a limit error means runs a lot, and when it decides wrong the effect adds up.
The engine has a clear rule for an empty quota. Our session notes state it: an empty quota stops the run, and the run picks up again three hours later. A draft is also never marked as given up for a reason that has nothing to do with its text. Running out of quota is that kind of reason. None of this helps if the quota error never gets classed as quota.
What the client did
When the Claude command line tool fails, it returns a result with an error flag, an error text and, sometimes, an HTTP status from the API. The client turned that into a kind in one line. If the status was 429, the kind was rate_limit. Only when it was not 429 did the client run the error text through classify, the function that knows what the different limit messages look like.
So classify could recognise a spend-limit message perfectly and still never be asked. A message like “You’ve hit your org’s monthly spend limit” came in with status 429, was labelled a rate limit, and was retried as one. Retrying does not refill a quota, so the run spent its retries on calls that could not succeed.
The same part of the client already carried a warning about trusting the wrong field. A comment written earlier says the tool’s subtype field “lies”: it said “success” while the error flag was true. The status code turned out to be a second field that did not mean what the code assumed. A 429 says “too many requests” at the HTTP level. It does not say whether the fix is to wait a minute or to wait for a monthly limit.
How we found it
It did not show up all at once. An earlier entry in our session notes, under open issues, recorded that a limit message had once been classed as rate_limit, not quota. A workaround was in place by then: the autopilot also stopped when it saw limit text. The note was honest about the gap: why classify had not matched the message “is not found”.
That was the wrong question. Whenever the status was 429, classify was never reached, because the status check ran first and short-circuited it. The workaround caught the symptom one layer up. The cause sat in the order of two checks in the client.
The fix commit removed that open issue from the notes. In its place, the description of how an empty quota is handled now says limit messages count as quota “also when they arrive as HTTP 429 (the text decides first)”.
The fix
The fix swaps the order. The client runs classify on the error text first. If the text says what kind of error this is, that answer wins. Only if the text matches nothing known does a 429 status fall back to being treated as a rate limit. Anything else is unknown.
A comment next to the new line explains why the order matters, and names the case that went wrong: the monthly spend limit message that “was once classed as a rate limit and retried on an empty quota.”
The test that holds the order
A fix like this is easy to undo. Someone tidies the function later, decides the status code is the more “reliable” signal, and moves it back to the top. So the same commit added a test called “the subscription limits are quota, whatever status they arrive with”.
It checks four messages:
- the monthly spend-limit message, which must be
quota - a “Claude AI usage limit reached” message with a reset timestamp, which must be
quota - a “You’ve hit your limit · resets 3pm” message, which must be
quota - a plain “429 Too Many Requests”, which must still be
rate_limit
The last case matters as much as the first three. The point is not that 429 never means rate limit. It is that the status alone can’t tell you which limit you hit, and the text can.
The test checks classify itself, so strictly it guards what the text check knows, not where it sits in the client. Its name and the comment in the client both record the reason for the order, which is the part most likely to get “simplified” away.
What we don’t know
The material doesn’t say how many runs retried on an empty quota before the fix, or how many retries each one used. Our notes recorded one case of a limit message being classed as a rate limit. We are not going to guess at a bigger number.
The rule
When an error can come from more than one cause under the same status code, read the message before you trust the code. Let the most specific signal decide, and use the generic one only as a fallback. When a workaround catches a symptom but the cause is marked “not found”, keep looking at what runs before the function you suspect. Once the order is fixed, put a test next to it that names the case that went wrong.