When a model API call fails, don't rush to retry. The order of judgment is: first look at the HTTP status code, then the error.type or error.code in the response body, and finally message. Following this order, most errors fall into two categories:
- Fix the request first: 400, 401, 404, and 429 when it's quota exhaustion. No amount of retrying will change the result.
- Wait a bit, it might recover: 429 rate limits, and 500, 502, 503, 504, and Anthropic's 529. Only these are worth retrying with backoff.
Separating these two categories is the starting point for writing retry logic, configuring alerts, and troubleshooting.

What error responses look like, and how to branch in code
Error response formats from OpenAI spec and vendors like Anthropic are broadly consistent: an HTTP status code plus a JSON structure, typically as follows:
{"error": {"message": "...", "type": "...", "code": "...", "param": null}}Fields vary across platforms; some include code, others rely mainly on type, but the principle is the same: branch based on status code and type/code, and use message only for human reading and logging. The wording of message may change across versions; if you string-match on it, a copy change can silently break your retry logic.
Also consider cases where JSON isn't available. Gateway errors like 502 and 504 may return HTML or an empty body. Don't let exception handling itself crash when parsing fails—fall back to the status code.
One by one: causes and handling
400: The request itself is invalid
This type falls under invalid_request_error, meaning the server considers the request invalid. Common causes: misspelled parameter names; passing parameters or invocation methods not supported by the current model; context_length_exceeded, i.e., input tokens plus reserved output tokens (e.g., max_tokens) exceeding the model's context window.
The fix is to correct the request. If the response has a param field, it often directly points to the problematic parameter—check it first. For context length exceeded, estimate length with a tokenizer tool, then truncate or compress history messages, or reduce the output reservation. For window and output limit calculations, see Model Context Window Comparison.
401: Authentication failure
Corresponds to authentication_error or invalid_api_key; these errors are not retryable. Check in the following order:
- Is the request header written as
Authorization: Bearer <key>, with correct prefix and space? - Is the key copied completely, without extra line breaks or spaces?
- Has the key been rotated or deleted?
- Does the environment variable read by the code actually have a value? If it works locally but returns 401 after deployment, it's often because the variable wasn't injected in the container or CI.
404: Model or path not found
Corresponds to not_found_error or model_not_found. Common causes:
- Wrong model name, especially mismatched version or date suffixes;
- The current key lacks permission to access this model;
- Incorrect endpoint path, e.g.,
base_urlmissing/v1, or an extra path segment.
404 is most common when migrating providers: base_url changes, but the model name still follows the original platform's format.
429: First distinguish rate limiting from quota exhaustion
The same 429 can mean two completely different things:
- Rate limit (
rate_limit_exceeded): Requests per minute (RPM), tokens per minute (TPM), or concurrency exceeded the limit in a short period. This is temporary; just wait and resend. - Quota exhausted (
insufficient_quota, or insufficient balance, spending limit reached, etc.): This is a billing issue; retrying will always fail—only top up or adjust limits.
Many retry implementations just "retry on 429". As a result, after quota is exhausted, the program keeps spinning in backoff, and alerts aren't triggered. Always separate the two by code/type, and when quota is exhausted, fail immediately and notify a human. Refer to your platform's documentation for specific values.
If rate limits occur frequently, your concurrency design doesn't match your quota; retries only treat the symptom. See How to Measure and Improve AI API Throughput to adjust concurrency and request pacing.
500, 502, 503, 504, 529: Temporary server-side failures
500 is an internal server error; 502 and 504 usually originate from upstream gateways or timeouts; 503 and Anthropic's 529 (overloaded_error) indicate the service is temporarily unavailable or overloaded. These errors are generally not caused by your code and are retryable.
However, "retryable" doesn't mean "retry immediately". When the server is overloaded, if all clients immediately resend, it only increases the load. For distinguishing 504 from your own client timeout, see How to Solve AI API Timeouts.
How to write retries without adding chaos
- Only retry retryable errors: rate-limit 429s and 5xx/529. Fail immediately on 400, 401, 404, and quota exhaustion.
- Exponential backoff with jitter: wait times increase as 1s, 2s, 4s with a cap, plus a small random offset each time to avoid many clients resending simultaneously.
- Prefer the server's guidance: if the response header provides a retry wait time (e.g.,
retry-after), wait accordingly. - Limit total attempts and total duration: for user-facing online requests, total wait time must be less than what the frontend can tolerate.
- Be aware of SDK built-in retries: official SDKs often have a small number of retries (e.g.,
max_retriesparameter). If you wrap another layer, retry counts multiply—choose one, or reduce the SDK's count.
Below is a minimal example using httpx to send requests:
import random
import time
import httpx
RETRYABLE_STATUS = {500, 502, 503, 504, 529}
QUOTA_CODES = {"insufficient_quota"} # 按所用平台文档补充
def post_with_retry(client: httpx.Client, url, payload, max_retries=4, base=1.0, cap=20.0):
for attempt in range(max_retries + 1):
resp = client.post(url, json=payload)
if resp.status_code < 400:
return resp.json()
try:
err = resp.json().get("error") or {}
except ValueError:
err = {} # 网关错误可能不是 JSON
code = err.get("code") or err.get("type")
if resp.status_code == 429:
retryable = code not in QUOTA_CODES
else:
retryable = resp.status_code in RETRYABLE_STATUS
if not retryable or attempt == max_retries:
resp.raise_for_status()
wait = resp.headers.get("retry-after")
if wait and wait.isdigit():
delay = float(wait)
else:
delay = min(cap, base * 2 ** attempt) * random.uniform(0.5, 1.0)
time.sleep(delay)Network-layer exceptions like connection timeouts should also be retried; the example omits that part.
When failures persist: circuit breakers and fallbacks
Backoff retries handle occasional failures. If the proportion of 5xx or 529 remains high over a period, continuing to retry just wastes time and worsens congestion. Use a circuit breaker: open when error rate exceeds a threshold, stop sending requests for a while and go straight to fallback logic; after cooldown, let a few requests probe first, and close once recovery is confirmed.
Fallback methods depend on the business: switch to a backup model, return cached results, queue non-real-time tasks for later, or give the user a clear message. Before switching to a backup model, confirm its context window and parameter support are compatible with the primary model; otherwise the fallback path itself will hit 400.
What to log
For each failure, log at least: status code, error.type, error.code, param, model name, request duration, and the request ID from response headers (if the platform returns one).
Alert by severity based on type/code:
- 401 and 404 should notify a human immediately; usually means configuration changed;
- 429 quota exhaustion should alert immediately;
- For rate limits and 5xx, look at proportion and duration; occasional ones can just be logged.
For a more complete troubleshooting order, including output truncation and suspected model downgrades, see AI API Troubleshooting.
How to troubleshoot after migrating to NexAIX
NexAIX provides an OpenAI-compatible endpoint; when migrating, just change base_url to https://api.nexaix.net/v1, and the error classification and retry logic above can be reused directly. A few things to check:
- On 404, check two things first: whether
base_urlincludes/v1, and whether the model name matches the model list. - Quotas and rate limits are public and isolated per API key, each key has its own quota. When multiple services share one key, they compete for the same limit; split into multiple keys so rate limits don't affect each other.
- Whether the gateway has its own error codes or request tracing headers is subject to the "Documentation" in the official website navigation.
NexAIX-官方博客
Comments(0)