The Problem: When Model APIs Are Unstable, the Call Chain Breaks First
When many teams first integrate a large model API, the initial code is usually simple: the application receives a request, constructs the messages, calls the model, and returns the result. This works in development, but in production, problems quickly surface.
As request volume increases, the call chain encounters 429 rate limits, 5xx server errors, connection timeouts, vendor overloads, request body too large, client disconnects, and quota exhaustion. The challenge is that these errors cannot be handled with a single strategy. Retrying all errors amplifies upstream pressure; not retrying at all exposes users to transient blips.
OpenAI’s documentation distinguishes between two types of 429 errors: requests too fast and quota exhausted, recommending exponential backoff with jitter for rate limits. Anthropic’s documentation lists errors like 429, 500, 504, and 529, noting that their official SDK retries connection errors, rate limits, and 5xx errors with limited exponential backoff. Gemini API’s troubleshooting guide classifies RESOURCE_EXHAUSTED as rate/quota issues related to RPM, TPM, RPD, or consumption limits. Production systems need more than a simple “retry on error”; they need to translate these signals into a stable client-side control plane.
This article is not about LLM Gateways or auto-scaling inference clusters. It’s about the LLM API Client Reliability Layer on the application side: a reliable call wrapper between business code and model providers, providing a unified boundary for error classification, timeouts, retries, degradation, auditing, and troubleshooting.
Core Principle: Classify First, Then Decide to Retry
LLM API errors can be broadly classified into five categories.
Non-Retryable Errors
Examples include authentication failures, insufficient permissions, non-existent models, parameter format errors, and requests exceeding maximum size. Retrying these errors is pointless; it only wastes tokens, slows responses, and pollutes logs. The correct approach is to fail fast and attribute the error to configuration, permissions, input validation, or deployment changes.
Transient Retryable Errors
Examples include network connection failures, server 500 errors, some 503 errors, temporary overloads, and temporary connection pool exhaustion. These are suitable for a small retry budget, typically 1-2 attempts. If they still fail, switch to a degradation path or return an explainable error.
Rate Limit Errors
A 429 error should not be treated simply as “try again.” If the response headers include reset or retry-after information, respect it. If not, use exponential backoff with jitter. Note that OpenAI’s documentation explicitly warns that unsuccessful requests may still count toward per-minute limits; continuously retrying in place won’t solve the rate limit issue.
Capacity and Overload Errors
Examples include Anthropic’s 529 overloaded_error or OpenAI’s 503 overloaded / Slow Down. These typically indicate global or local high load. The client should reduce concurrency, extend backoff, and trigger circuit breaking, rather than having each business thread retry independently.
Business-Side Cancellation and Timeouts
When a user closes a page, an upstream HTTP timeout occurs, a task is cancelled, or a gateway disconnects, a successful model call may no longer be valuable to the current business operation. The client must propagate the cancellation signal downstream and record such results as cancelled, not mixed in with failed.
| Error Category | Typical Error Codes | Handling Strategy |
|---|---|---|
| Non-Retryable | 401, 403, 404, 413 | Fail fast, attribute to config/permissions/input validation |
| Transient Retryable | Connection failure, 500, some 503 | 1-2 small budget retries, degrade on failure |
| Rate Limit | 429 | Respect retry-after, exponential backoff + jitter |
| Overload | 529, 503 overloaded | Reduce concurrency, extend backoff, trigger circuit breaker |
| Cancellation/Timeout | Client disconnect, upstream timeout | Propagate cancellation signal, record as cancelled |
Request Ledger: Making Every Model Call Traceable, Auditable, and Compensable
The worst reliability problem is having “just one error log.” It’s recommended to establish a request ledger for each logical request, recording each actual attempt to call the model API.
A minimal ledger can include these fields:
logical_request_id user_or_tenant_id operation_type provider model
prompt_hash input_token_estimate max_output_tokens client_timeout_ms
attempt_id attempt_no started_at finished_at status error_class
provider_request_id retry_after_ms latency_ms output_token_count
fallback_used final_result_pointer
Here, logical_request_id represents a single business-level call, e.g., “generate a summary for this ticket.” attempt_id represents an actual request to the vendor API. These two should not be conflated. One logical request may have multiple attempts, or may switch to a different model after failure.
The request ledger is not a replacement for tracing. It fills the troubleshooting dimensions specific to LLM calls: model, token estimation, parameter summary, error type, retry count, degradation path, and final artifact location. Without this ledger, it’s difficult to determine post-hoc whether a failure was due to vendor rate limiting, application-layer timeout, oversized request body, or a single tenant instantly exhausting a shared TPM.
Timeout Boundaries: Don’t Give All the Time to the Model
Large model calls are often slow in the tail. Many incidents are not about high average latency, but about a few requests getting stuck, gradually consuming application threads, connection pools, and queues.
It’s recommended to split a single business request into three time budgets:
total_user_budget = 12s
├─ input_build_budget = 1s
├─ model_call_budget = 8s
└─ postprocess_budget = 3s
Within the model call, further split into per-attempt timeout and overall retry budget:
model_call_budget = 8s
├─ attempt_1 timeout = 4s
├─ backoff = 300ms ~ 900ms
├─ attempt_2 timeout = 2.5s
└─ fallback_or_fail = remaining budget
The core value of this approach is preventing “retry succeeded, but the user has already given up.” For online interactive scenarios, the retry budget must consider not only success rate but also whether the final response is still within the user’s acceptable time. For background tasks, the time budget can be extended, but the task should be placed in a queue to avoid occupying synchronous request threads.
Retry Budget: Limit Both Count and Storm Potential
A practical retry strategy can be implemented like this:
type ErrorClass =
| "bad_request" | "auth_error" | "permission_error"
| "rate_limited" | "quota_exhausted" | "request_too_large"
| "timeout" | "connection_error" | "server_error"
| "overloaded" | "cancelled" | "unknown";
type RetryDecision = {
retryable: boolean;
delayMs?: number;
reason: string;
};
function decideRetry(
errorClass: ErrorClass,
attemptNo: number,
remainingMs: number
): RetryDecision {
if (remainingMs < 1000) {
return { retryable: false, reason: "not_enough_time_budget" };
}
if (["bad_request", "auth_error", "permission_error",
"request_too_large", "quota_exhausted", "cancelled"]
.includes(errorClass)) {
return { retryable: false, reason: "non_retryable_error" };
}
if (attemptNo >= 2) {
return { retryable: false, reason: "retry_budget_exhausted" };
}
if (["rate_limited", "timeout", "connection_error",
"server_error", "overloaded"].includes(errorClass)) {
const base = Math.min(2000, 300 * Math.pow(2, attemptNo));
const jitter = Math.floor(Math.random() * base);
return {
retryable: true,
delayMs: base + jitter,
reason: "transient_retry"
};
}
return { retryable: false, reason: "unknown_error" };
}
This logic is not final, but it embodies three production principles:
- Non-retryable errors must fail fast — especially parameter errors and oversized requests; retrying won’t change the outcome.
- Retries must include random jitter — if all clients retry simultaneously after 1, 2, and 4 seconds, the vendor will be hit by another surge the moment it recovers.
- Retry budgets must be centrally governed — don’t let the business service, SDK, HTTP client, and queue worker each retry three times. Three layers each retrying three times could result in 27 calls in the worst case, and it’s very difficult to identify which layer amplified the problem during troubleshooting.
Degradation Strategies: Not All Failures Need a 500 Response
The degradation strategy after an LLM call failure depends on the business type:
- For auxiliary tasks like customer service summaries, search rewrites, and tag classification, return “temporarily unavailable” and preserve the original input, allowing the user to continue the main flow.
- For scenarios heavily dependent on generated results, switch to a smaller model, shorten the output length, disable expensive reasoning, or return a previously cached stable result.
- For high-risk scenarios like compliance, healthcare, and finance, do not silently switch models for the sake of success rate; degradation must be explicitly audited.
A simple but effective degradation sequence is:
primary model
→ same model with shorter max_output_tokens
→ fallback model with explicit quality label
→ async retry job
→ user-visible graceful failure
A critical note: results from the fallback model must not be mixed with results from the primary model in statistics. Otherwise, when analyzing “why quality dropped,” you’ll only see an overall success rate curve, hiding the underlying model switch.
Vendor Response Headers: Turning Rate Limit Info into Scheduling Input
OpenAI’s rate limit documentation lists response headers like x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests, x-ratelimit-limit-tokens, x-ratelimit-remaining-tokens, and x-ratelimit-reset-tokens. Clients should not only read these on error.
A more robust approach is to write response headers into a local vendor state table:
provider_model_key remaining_requests remaining_tokens
reset_requests_at reset_tokens_at last_429_at
last_overload_at current_client_concurrency
When remaining tokens drops rapidly, the client can proactively reduce concurrency, compress output length, or move non-real-time tasks to an offline queue. This way, rate limiting is no longer a passive error but an input to scheduling.
Common Pitfalls
Pitfall 1: The SDK Already Retries, So the Business Layer Doesn’t Need To
Official SDK default retries usually only handle local transient errors. The business layer still needs to control total timeout, total retry budget, degradation paths, request ledgers, and tenant-level concurrency. Otherwise, the SDK may retry successfully, but the user experience may already have failed.
Pitfall 2: All 429 Errors Should Use Exponential Backoff
A 429 error can indicate either excessive request rate or exhausted budget/quota. The former can be backed off; the latter should stop and alert. Treating quota exhausted as rate limited leads to meaningless retries.
Pitfall 3: Only Record Final Failures, Not Intermediate Attempts
If only the final status is recorded, the system misses many early signals. For example, a request might ultimately succeed, but the first two attempts returned 503, indicating upstream instability. If these attempts are not logged, troubleshooting will be significantly delayed when a real failure occurs.
Pitfall 4: All Businesses Share a Single Concurrency Pool
When multiple tenants or teams share the same API key without tenant-level concurrency and budget control, a low-priority batch task can drag online requests into rate limiting. Even if the vendor rate limits at the organization or project level, the client should implement local distribution and isolation.
Deployment Checklist
Before going live, at least check the following items:
Error Classification
Ensure that 400, 401, 403, 404, 413, 429, 500, 503, 504, 529, connection errors, and client cancellations are all mapped to a clear internal error_class. Unknown errors should not default to infinite retries.
Timeout Configuration
Differentiate between connection timeout, first-byte timeout, overall response timeout, and total business budget. For streaming interfaces, also differentiate between first-token timeout and chunk interval timeout.
Retry Budget
Unify limits on maximum attempts, total retry duration, backoff cap, and jitter. Check for duplicate retries across SDK, HTTP client, and queue worker.
Request Ledger
Ensure every logical request and every attempt is recorded, and can be correlated with provider request ID, model, parameter summary, token estimation, error type, and final status.
Degradation Paths
Clearly define which businesses can be degraded, which model to fall back to, whether user notification is needed, and whether auditing is required. High-risk businesses should not be silently degraded.
Metrics and Alerts
Monitor at least the following dimensions:
- Success rate, error category distribution, 429 ratio, 5xx ratio
- Timeout rate, cancellation rate
- Average attempts, post-retry success rate, fallback rate
- P95/P99 latency, per-tenant token consumption
Summary
LLM API client reliability governance is not solved by simply “adding retries.” It requires building a control plane that integrates error classification, timeout boundaries, retry budgets, request ledgers, degradation strategies, and vendor state awareness. The core ideas can be summarized in three points: classify before retrying, use a ledger instead of scattered logs, and turn vendor rate limit headers into scheduling input, not passive alerts. With these in place, the call chain can remain stable amidst the inevitable fluctuations of model APIs.