Every team that integrates multiple model vendors eventually trips over the inconsistent ways APIs report failures. This guide to llm api error codes by provider maps the real response shapes from OpenAI and Anthropic, then gives you a concrete pattern for handling them in production. You will leave with a retry strategy and a code skeleton that survives provider outages.
The baseline: HTTP status codes
All LLM providers speak HTTP, so the first signal is the status code. The meaning of 429 is universal: you are sending too much traffic. 401 and 403 mean authentication or permission failed. 400 and 422 indicate the request itself is malformed. 500 and 503 are server-side faults.
The trap is assuming those codes carry the same retry semantics across vendors. A 429 from OpenAI might include a rate_limit_exceeded code with a reset timestamp; Anthropic might return the same status with an overloaded_error type and expect a different backoff. Your client must read the body, not just the status line.
OpenAI error envelope
OpenAI wraps every failure in an error object. The type field is the stable identifier; code is sometimes more specific.
{
"error": {
"message": "Rate limit reached for gpt-4o-mini in organization org-xyz on tokens per min.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null
}
}
Relevant type values you will see in practice:
invalid_request_error(400/422): bad parameters, unknown model, context length exceeded.authentication_error(401): missing or invalid API key.permission_error(403): key lacks access to the model or endpoint.rate_limit_error(429): throughput or token quota exceeded.api_error(500): something broke on OpenAI’s side.service_unavailable_error(503): transient infrastructure issue.
The code field often disambiguates within rate_limit_error (rate_limit_exceeded vs insufficient_quota). Ignore it and you will retry a hard quota failure forever.
Anthropic error envelope
Anthropic uses a different shape: a top-level type: "error" with a nested error object. The nested type is the canonical code.
{
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded"
}
}
Key Anthropic error types:
invalid_request_error(400): malformed body, bad tool schema.authentication_error(401): bad key.permission_error(403): not authorized for model.not_found_error(404): unknown model or resource.request_too_large(413): prompt exceeds max size.rate_limit_error(429): standard throttling.overloaded_error(529): Anthropic’s own gateway is saturated; retry with longer backoff than a normal 429.api_error(500): unhandled server fault.
Note the 529 status. It is not RFC-standard, but Anthropic uses it deliberately to signal load shedding. Treat it as a heavier 429.
Mapping llm api error codes by provider to actions
When you normalize the two envelopes, the decision tree is straightforward:
| Normalized code | Status | Action |
|---|---|---|
auth_error |
401/403 | Stop, rotate key or fix permissions. Never retry. |
invalid_request |
400/413/422 | Stop, log the message, fix caller. |
not_found |
404 | Stop, model name typo or unavailable. |
rate_limit |
429 | Retry with exponential backoff + jitter. |
overloaded |
529 | Retry with longer backoff, lower concurrency. |
server_error |
500/503 | Retry once or twice, then escalate. |
The llm api error codes by provider diverge in naming, but the above mapping covers 95% of production incidents.
Building a unified parser
Write one function per provider, then flatten into an internal exception. Keep the original provider and raw body for debugging.
class ProviderError(Exception):
def __init__(self, status: int, code: str, message: str, provider: str, raw: dict):
super().__init__(f"{provider}:{code} {message}")
self.status = status
self.code = code
self.message = message
self.provider = provider
self.raw = raw
def parse_openai(status: int, body: dict) -> ProviderError:
err = body.get("error", {})
code = err.get("code") or err.get("type")
return ProviderError(status, code, err.get("message", ""), "openai", body)
def parse_anthropic(status: int, body: dict) -> ProviderError:
err = body.get("error", {})
code = err.get("type")
return ProviderError(status, code, err.get("message", ""), "anthropic", body)
def normalize(err: ProviderError) -> str:
if err.status in (401, 403):
return "auth_error"
if err.status in (400, 413, 422):
return "invalid_request"
if err.status == 404:
return "not_found"
if err.status == 429:
return "rate_limit"
if err.status == 529:
return "overloaded"
if err.status >= 500:
return "server_error"
return "unknown"
This takes the llm api error codes by provider and reduces them to six retry-relevant buckets.
Retry logic that doesn’t melt the system
Exponential backoff with full jitter is non-negotiable. A fixed 1s sleep amplifies thundering herds during provider incidents.
import random, time
def backoff_sleep(attempt: int, base: float = 0.5, cap: float = 30.0):
delay = min(cap, base * (2 ** attempt))
time.sleep(delay * random.random())
For overloaded (529), double the base. For rate_limit, honor any Retry-After header if present. Cap total attempts at 5; beyond that, fail open to a fallback model or return a cached response.
Tradeoff: aggressive retries improve yield but increase p99 latency. Measure tail latency before tuning cap.
Using a gateway to reduce surface area
If you route through a gateway such as n4n.ai, you get automatic fallback when a provider is rate-limited or degraded, but you should still inspect the forwarded error type to decide whether to surface or retry. The gateway may normalize status codes, yet the underlying provider field remains useful for alerting.
A gateway does not excuse you from parsing bodies. Quota errors (insufficient_quota) are not fixed by fallback; they require a billing action.
Common pitfalls
Assuming 429 is always transient. OpenAI returns rate_limit_exceeded for per-minute limits (retryable) but insufficient_quota for billing caps (fatal). Check the code.
Swallowing the message field. The message often contains the specific parameter that failed validation. Log it.
Treating Anthropic overloaded_error as a normal 429. Its 529 demands longer backoff. If you retry at 429 cadence, you extend the outage for everyone.
Mixing provider and gateway errors. If a gateway returns a 502 because it could not reach Anthropic, that is not an Anthropic api_error. Tag the source.
No dead-letter queue. Failed batch jobs should land in a queue for replay, not vanish.
Ordered path to production readiness
- Wrap every LLM call in a parser that converts the vendor body into
ProviderError. - Normalize to the six buckets above.
- Implement jittered backoff for
rate_limitandoverloaded; never retryauth_errororinvalid_request. - Set attempt caps and emit metrics tagged by
providerandcode. - Add fallback routing (via code or gateway) for
server_errorandoverloaded. - Alert on
insufficient_quotaandpermission_errorimmediately—those are human problems. - Periodically diff the error schemas; vendors add codes without notice.
The llm api error codes by provider will keep drifting, but a thin normalization layer keeps your application stable. Ship the parser first, the retries second, and the dashboards last.