Your LLM Proxy Retries the Wrong Errors
What an LLM proxy has to decide on every failure: limits are per organization, which statuses to retry, why nothing is retried after the first streamed byte, and the headroom headers the API already sends.
Key takeaways
- Anthropic enforces rate limits per organization, so failing over between two keys from the same organization adds no capacity.
- Track health per rate-limit bucket, not per key, and cool the bucket when it returns 429.
- Retry what another attempt could survive: 429, 5xx, network errors, and 401 or 403 on a different key; a 400 fails the same way everywhere.
- retry-after sets the wait exactly when present, and a 401 or 403 disables the key until a person fixes it.
- Once the first streamed byte reaches the client, a failure must end the stream with an error event, never a clean ending.
- The anthropic-ratelimit headers on every response, 429s included, give first-hand headroom for the current window at no extra cost.
Contents
FAQ
Does rotating API keys get around a rate limit?
Not within one organization: its keys share one rate-limit bucket. Keys in separate organizations have separate limits, but using them to get around a limit may break the provider's terms of use.
Should a proxy retry a 400 error?
No. A 400 means the request itself is invalid, so every backend rejects it the same way. Return it to the client unchanged.
What should a proxy return when every backend fails?
The last upstream error as it came, with its status and body, rather than a generic 503 of its own, so the client SDK can decide how to back off.
Sources
Only you and the owner see your answer and rating. Send private feedback
Enjoyed it? Support fx-blog with a coffee.
Related posts
Your LLM Demo Works. What Every Call Needs in Production
One timestamp in the system prompt and every call misses the cache. Pricing each call, making failures loud, logging per feature, and treating outside text as untrusted.
RAG That Works: Why Retrieval Matters More Than the Prompt
Most bad RAG answers are retrieval problems. How RAG works end to end, the three parts of the grounded prompt, how to measure retrieval alone, and when long context is enough.
Did Your Prompt Change Help? Prove It with an Eval
A model upgrade can change behaviour with no error. What an eval is, the three graders, the held-out slice, and how evals make upgrades routine.
Comments
Oldest firstNo comments yet. Start the conversation.
Part 5 ended with the test-case generator and every other service sharing one rate limit, and a proxy that must decide, on every failure, whether another attempt can succeed.
Get it wrong three ways: A 400 retried three times returns the same error three times. Failover to a second key in the same organization hits the same 429. A retry after the first streamed byte hands the client two half-answers as one message.
This series, in six parts:
One question per failure
The decision deserves tests, so I wrote it as pure functions:
A 401 or 403 is there because a different key may still work.
How long a backend rests depends on why:
429withretry-after: N429spend cap (noretry-after)5xx, network error401,403400, other 4xxI disable on a 401 rather than cool, because a revoked key never heals on a timer. And
retry-afteralways beats my own backoff: the docs say earlier retries fail, so any wait I invent can only waste requests.Rate limits belong to the organization
Anthropic enforces rate limits per organization. Two keys from one organization share one bucket, so failing over between them walks into the same 429. Keys in different organizations do not.
So the proxy tracks health per bucket, not per key: keys from one organization form one pool, and a 429 cools the pool. I pick the least recently used healthy pool.
Nothing is retried after the first byte
Before the first byte, a failure is invisible and the proxy can send the same body elsewhere. After it, the status is committed and the client has parsed
message_start, the event that opens the stream. A replay pushes a secondmessage_startinto what the client thinks is one message, and the SDK either throws or glues two half-answers together.So a mid-stream failure ends the stream with an error the client can see:
errorevent and end. Never close quietly.Try it
The test-case generator went from one call to a loop, an eval, your own docs, a nightly run and a proxy that fails honestly. Build one piece this week? Make it the eval from part 3. Without it, every other change is a guess.
Read this with your AI