Contents
Figure 1: LiteLLM Errors: Your 401 Might Be a Budget Problem — the status code hints, the message tells the truth
You see a 401 and rotate the API key. Nothing changes. Why? Because in LiteLLM's own docs, some "authentication" errors are actually budget errors, and some "429s" are not provider rate limits at all. Reading the status code alone sends you down the wrong path.
I did not run LiteLLM, since it is not installable here. Every message and setting below comes from LiteLLM's docs pages. My script is a stand-in that classifies the docs' own examples.
First Question: Who Sent the Error?
LiteLLM's diagnosis page gives one rule: "If the error contains <Provider>Exception, it's from the provider." [1] The page also carries the mapping table I expected to be missing [1]:
| Message contains | Source |
|---|---|
AnthropicException |
Anthropic |
OpenAIException |
OpenAI |
AzureException |
Azure |
BedrockException |
AWS Bedrock |
VertexAIException |
Google Vertex AI |
| No provider name | LiteLLM AI Gateway |
On the proxy, failed calls return an error object with four fields [2]:
| Field | Meaning |
|---|---|
message |
Human-readable description; for provider failures it embeds both the LiteLLM and provider exception names |
type |
Error category, such as throttling_error or budget_exceeded |
param |
The offending parameter, when the request is at fault |
code |
The status code, as a string |
The docs' rate-limit sample does contain the provider name, which settles a question my first read left open [2]:
{
"error": {
"message": "litellm.RateLimitError: RateLimitError: OpenAIException - Rate limit reached for gpt-6-luna in organization org-abc on requests per min (RPM): Limit 3, Used 3.",
"type": "throttling_error",
"param": null,
"code": "429"
}
}
So that sample is a provider limit, and the rule classifies it. A message without a provider name points at your client or the proxy. Read the wording too, because expired_key and budget_exceeded never mention a provider [2].
Authentication Errors
Start with the key your client actually sends. The proxy expects Authorization: Bearer <key>, and the master key must start with sk-. Virtual keys come from /key/generate. If you set litellm_key_header_name, the proxy reads the key from that header instead of Authorization [5].
Then check the things that turn a good key into a failing one. The docs do name the errors, which saves you guessing [2]:
| Gateway error type | Status | Cause |
|---|---|---|
token_not_found_in_db |
401 | The virtual key does not exist |
expired_key |
401 | The key's expires timestamp has passed |
auth_error |
401 | Generic authentication failure, including JWT validation |
key_model_access_denied |
403 | The key's models list excludes the requested model |
team_model_access_denied and friends |
403 | The same restriction at team, user, org or project level |
auth_provider_unavailable |
503 | The identity provider's JWKS endpoint is unreachable |
If the message names a provider, the problem is the provider key in your deployment config, not the client's key [1].
The Budget Trap
Budget errors are the surprise. The users page shows these examples [3]:
| Budget | What the client sees |
|---|---|
| Key | {"detail":"Authentication Error, ExceededTokenBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07"} |
| Customer | {"error":{"message":"ExceededBudget: End User=ishaan3 over budget...","type":"auth_error","param":"None","code":401}} |
| Internal user | type: budget_exceeded, code: 400 |
| Per-model on a key | budget_exceeded, code: 400 |
| Per-model on a user | budget_exceeded, code: 429 |
So a 401 can be a spent budget and a 429 can be a budget too. Look for ExceededBudget or ExceededTokenBudget in the message before you touch any key. The page does not explain why the per-model case differs between a key and a user, so treat those two codes as observed rather than principled [3].
Also note that budgets need a Postgres database. Without one, the docs say the global budget "fails open", and the gateway-side budget_exceeded type is documented as a 429 for key, team, user or session caps [2][3].
Rate Limits: Find Which Limit Fired
A rate limit can come from three places, and the fix differs for each.
- The provider. The message names the provider's own limit. Slow down, add capacity or add a fallback deployment.
- LiteLLM key, user or team limits. These are
rpm_limit,tpm_limitandmax_parallel_requests, plus per-model versions. Raise them or spread traffic. TPM counts total tokens by default, andtoken_rate_limit_typechanges that [3]. - Per-model limits with a fallback. By default, if a fallback is configured, the client gets a 200 from the fallback model. Setting
disable_fallbacks_on_per_model_rate_limits: trueturns the limit into a hard cap that returns 429 [3].
Two more details matter if you run more than one proxy instance. Counters live in Redis, and if Redis is unreachable each instance falls back to its own memory, so a limit can effectively multiply by the number of instances [3]. fail_closed_rate_limit_enforcement: true rejects requests instead, with a 503 rather than a 429, so you can tell a Redis outage from a real rate limit [3].
The users page still does not explain how your own limits interact with the provider's, which is where most confusing double-429s come from.
Timeouts: Which One Is Winning?
The timeout page lists several settings. The ones to know [4]:
| Setting | Scope |
|---|---|
timeout (under litellm_params) |
Caps the complete response; the example is 300 |
stream_timeout |
Caps the wait for the first chunk; the example is 30 |
request_timeout |
Proxy-wide default via litellm_settings.request_timeout or REQUEST_TIMEOUT, used only when no router or deployment timeout is set |
timeout in the call, or x-litellm-timeout |
Per-request override |
timeout_by_service_tier |
Per-tier values in seconds; the proxy refuses to start if they are not numbers |
Per attempt, precedence is: request timeout or header, then the tier map, then the deployment timeout, then router and global defaults [4]. For long streaming responses, keepalive_seconds can send SSE ping comments during silent gaps, and the effective value is clamped to 1-300 seconds [4].
The page gives no numeric defaults, and it does not name the exception or status returned on a timeout [4]. The error reference fills half that gap: a 408 means the call exceeded the timeout in force, the message reports both the configured timeout and the elapsed time, and it is retryable with backoff [2]. "mock_timeout": true in the request body simulates one, but only on /chat/completions and /completions [4].
A Fast Triage Routine
- Read the message text, not just the status code.
- Look for
ExceededBudgetfirst. It is cheap to check and easy to miss. - Look for a provider name to split provider from gateway.
- For rate limits, identify which limit fired: provider, key, team or per-model.
- For timeouts, work out which of the four settings applies.
- Log the full error object. Half of these fixes come from one field you did not save.
Common Pitfalls
- Rotating a key in response to a budget 401. The credential was never the problem.
- Raising
rpm_limitwhen the provider is throttling you. Your ceiling is not the bottleneck. - Assuming a per-model limit returned an error. A fallback turned it into a 200.
- Setting a tier timeout as a string. The proxy will not start.
- Blaming the provider for
expired_key. No provider was involved in that failure. - Testing timeouts on
/v1/responses.mock_timeoutonly works on/chat/completionsand/completions. - Debugging without the full error body.
typecarries the diagnosis thatcodehides. - Forgetting the header setting. If
litellm_key_header_nameis configured,Authorizationis not where the key comes from.
Best Book to Read
| Cover | Book | Description | Get it |
|---|---|---|---|
![]() |
Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems | rule one is "understand the system", and the rest are the habits that turn a 401 into a five-minute fix. | View on Amazon |
As an Amazon Associate I earn from qualifying purchases. Links on this page are affiliate links, and I may earn a commission at no extra cost to you.
Wrapping Up
Most LiteLLM errors have the same shape: a status code that hints and a message that tells the truth. Check budgets before keys, identify the limit before raising it, and know which timeout wins before editing one.
Next time a 401 shows up, read the message first and rotate the key second.
If you have not set retry and fallback behaviour yet, the retries and fallbacks guide covers which errors deserve another attempt. Spend policy lives in the virtual keys and budgets guide.
Frequently Asked Questions
Why does a LiteLLM 401 keep happening after I rotate the API key?
Because the 401 is often not about a key at all. The gateway uses 401 for token_not_found_in_db, expired_key and a generic auth_error, but a customer budget breach returns 401 with type auth_error and an ExceededBudget message, and a key budget breach returns an Authentication Error detail containing ExceededTokenBudget. Search the message for ExceededBudget or ExceededTokenBudget before you touch any credential.
How do I tell whether an error came from the provider or from the LiteLLM gateway?
Use the docs' quick rule: if the message contains a provider exception name such as AnthropicException, OpenAIException, AzureException, BedrockException or VertexAIException, the provider sent it. A message with no provider name points at LiteLLM itself. The error reference goes further and lists gateway-generated error types like expired_key, key_model_access_denied and budget_exceeded, which never involve a provider.
What do the different LiteLLM 429 responses mean?
A 429 can be a provider rate limit, a LiteLLM rpm_limit, tpm_limit or max_parallel_requests ceiling, or a budget_exceeded error, which the gateway returns with status 429 when a key, team, user or session spend cap is hit. The error reference distinguishes them by error.type: throttling_error for rate limits and budget_exceeded for spend caps. Check the message text before you raise any limit.
Which timeout setting applies to a LiteLLM request?
Per attempt, LiteLLM picks the first value that is set: a timeout sent with the request, either the timeout parameter or the x-litellm-timeout header, then timeout_by_service_tier for that tier, then the deployment's own timeout under litellm_params, then the router and global defaults such as litellm_settings.request_timeout or the REQUEST_TIMEOUT environment variable. Streaming requests follow the same order using stream_timeout.
Why does my client return 200 when I expected a per-model rate limit error?
Because a per-model limit is not a hard cap by default. If router_settings.fallbacks has an entry for that model, LiteLLM retries on the fallback model and the per-model limit does not follow it, so the client sees a 200 from the fallback. Set disable_fallbacks_on_per_model_rate_limits to true to turn per-model key, team, organization and project limits into hard caps that return 429.
What does a 503 from LiteLLM mean when Redis or the identity provider is down?
It depends on which 503. A provider 503 means the provider answered that it is unavailable or overloaded, and it is retryable with backoff. A gateway 503 can mean auth_provider_unavailable, when the identity provider's JWKS endpoint is unreachable, or it can come from fail_closed_rate_limit_enforcement rejecting a request whose counters cannot be verified against Redis. That last case uses 503 rather than 429 so clients can tell an outage from a rate limit.
Sources
[1]LiteLLM, Diagnosing Errors — Provider vs Gateway — the<Provider>Exceptionquick rule, the provider-name mapping table for Anthropic, OpenAI, Azure, Bedrock and Vertex AI, and the "no provider name" case pointing at the LiteLLM gateway: docs.litellm.ai/docs/proxy/error_diagnosis[2]LiteLLM, Error Reference — themessage/type/param/codepayload, thethrottling_error429 sample containingOpenAIException, the status-code table with origin and retry guidance including 408 and 503, the note that an unreachable provider surfaces as 500 withConnection error, and the gateway error types table withtoken_not_found_in_db,expired_key,auth_error,auth_provider_unavailable,key_model_access_deniedandbudget_exceededmapped to 429: docs.litellm.ai/docs/proxy/error_reference[3]LiteLLM, Budgets, Rate Limits — the key-budgetAuthentication Error, ExceededTokenBudgetdetail, the customer budgetauth_error401, the internal-userbudget_exceeded400, the per-model 400 on a key versus 429 on a user, the fail-open global budget without a database,token_rate_limit_type, the per-model fallback behaviour withdisable_fallbacks_on_per_model_rate_limits, and the Redis counter andfail_closed_rate_limit_enforcement503: docs.litellm.ai/docs/proxy/users[4]LiteLLM, Timeouts —timeoutandstream_timeoutunderlitellm_params,litellm_settings.request_timeoutandREQUEST_TIMEOUTas the proxy-wide default, thex-litellm-timeoutheader,timeout_by_service_tiervalidation, the per-attempt precedence order,keepalive_secondsclamped to 1-300 seconds, andmock_timeoutlimited to/chat/completionsand/completions: docs.litellm.ai/docs/proxy/timeout[5]LiteLLM, Config settings —litellm_key_header_name, which reads LiteLLM keys from a custom header and defaults to readingAuthorization: docs.litellm.ai/docs/proxy/config_settings
