Rate limits
Rate limits
Two different things share the 429 status, and confusing them leads to the wrong fix.
Your own route limit
Each route has a per-key limit. Exceeding it answers:
{
"error": {
"code": "gateway_rate_limited",
"message": "Rate limit exceeded (gateway_rate_limited)"
}
}
This is about how fast you are calling. Backing off fixes it.
Upstream throttling
A 429 rate_limited means a provider throttled the request. The Retry-After header and
the message both carry the wait, and x-should-retry is true.
A 429 pool_exhausted means every account behind that model is throttled at once. The
message names the soonest cooldown and carries the same Retry-After.
pool_exhausted is a wait, not a reason to switch modelsThe model is fine. Switching costs you a model choice, and often a worse one, for a wait
you could have sat through. Read Retry-After and retry the same request.
Reading the difference
| Signal | Meaning |
|---|---|
code: gateway_rate_limited |
you are calling too fast; slow down |
code: rate_limited |
one provider is throttling; wait and retry |
code: pool_exhausted |
every provider account is throttled; wait and retry |
x-should-retry: true |
the gateway believes a retry can succeed |
Retry-After |
seconds to wait, when it is known |
Retry with backoff, not in a tight loop
A tight retry loop against a 429 costs you nothing in money and everything in
throughput: it keeps the limit saturated for as long as you keep calling. Respect
Retry-After when it is present, and use exponential backoff with jitter when it is not.
import random, time, httpx
def call_with_retry(payload, key, attempts=4):
for n in range(attempts):
r = httpx.post(
"https://hisoka.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {key}"},
json=payload,
timeout=120,
)
if r.status_code < 400:
return r.json()
body = r.json().get("error", {})
if r.headers.get("x-should-retry") != "true":
raise RuntimeError(f"{body.get('code')}: {body.get('message')}")
wait = float(r.headers.get("Retry-After") or 0) or (2 ** n + random.random())
time.sleep(min(wait, 30))
raise RuntimeError("retries exhausted")
Bursts are handled upstream
The gateway spreads a burst across the accounts behind a model rather than sending
everything to the first one, and an account that fails is benched briefly so a client
burst does not all land on a broken connection. Most short blips never reach you.
A 503 model_unavailable is the case where they did, and it is worth a backoff retry
before anything else.
What is not a rate limit
502 upstream_error is a provider failure, not throttling. Retrying is still right, but
a tight loop makes it worse.
504 request_timeout is our clock. The fix is a longer budget or a shorter prompt.
A burst of 400s across every account at once is your payload, not the pool. A
request-shaped failure is deterministic, so it fails identically everywhere; the gateway
stops at the first one rather than multiplying it.