Sam Austinon October 27, 202616 min read

LiteLLM Errors: Your 401 Might Be a Budget Problem

LiteLLM Errors: Your 401 Might Be a Budget Problem
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.

  1. The provider. The message names the provider's own limit. Slow down, add capacity or add a fallback deployment.
  2. LiteLLM key, user or team limits. These are rpm_limit, tpm_limit and max_parallel_requests, plus per-model versions. Raise them or spread traffic. TPM counts total tokens by default, and token_rate_limit_type changes that [3].
  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: true turns 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

  1. Read the message text, not just the status code.
  2. Look for ExceededBudget first. It is cheap to check and easy to miss.
  3. Look for a provider name to split provider from gateway.
  4. For rate limits, identify which limit fired: provider, key, team or per-model.
  5. For timeouts, work out which of the four settings applies.
  6. Log the full error object. Half of these fixes come from one field you did not save.

Common Pitfalls

  1. Rotating a key in response to a budget 401. The credential was never the problem.
  2. Raising rpm_limit when the provider is throttling you. Your ceiling is not the bottleneck.
  3. Assuming a per-model limit returned an error. A fallback turned it into a 200.
  4. Setting a tier timeout as a string. The proxy will not start.
  5. Blaming the provider for expired_key. No provider was involved in that failure.
  6. Testing timeouts on /v1/responses. mock_timeout only works on /chat/completions and /completions.
  7. Debugging without the full error body. type carries the diagnosis that code hides.
  8. Forgetting the header setting. If litellm_key_header_name is configured, Authorization is not where the key comes from.

Best Book to Read

CoverBookDescriptionGet it
Cover of “Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems” Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problemsby David J. Agans 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>Exception quick 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 — the message/type/param/code payload, the throttling_error 429 sample containing OpenAIException, the status-code table with origin and retry guidance including 408 and 503, the note that an unreachable provider surfaces as 500 with Connection error, and the gateway error types table with token_not_found_in_db, expired_key, auth_error, auth_provider_unavailable, key_model_access_denied and budget_exceeded mapped to 429: docs.litellm.ai/docs/proxy/error_reference
  • [3] LiteLLM, Budgets, Rate Limits — the key-budget Authentication Error, ExceededTokenBudget detail, the customer budget auth_error 401, the internal-user budget_exceeded 400, 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 with disable_fallbacks_on_per_model_rate_limits, and the Redis counter and fail_closed_rate_limit_enforcement 503: docs.litellm.ai/docs/proxy/users
  • [4] LiteLLM, Timeouts — timeout and stream_timeout under litellm_params, litellm_settings.request_timeout and REQUEST_TIMEOUT as the proxy-wide default, the x-litellm-timeout header, timeout_by_service_tier validation, the per-attempt precedence order, keepalive_seconds clamped to 1-300 seconds, and mock_timeout limited to /chat/completions and /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 reading Authorization: docs.litellm.ai/docs/proxy/config_settings

What are You Looking For?

esc