LiteLLM budgets: set limits and verify request blocking
LiteLLM budget not working? Configure max_budget on the virtual key your app actually uses, then run a bounded test that proves requests are rejected for budget reasons — not just displayed in the admin UI.
Jump to the verification example (local network_mock stack, no paid provider by default) or the troubleshooting table.
Hub: Tokenminning with LiteLLM.
This page answers litellm max_budget, litellm budget exceeded, and litellm budget not working in one place.
Budgets vs rate limits vs alerts
| Control | What it blocks | Where to configure | Proof signal |
|---|---|---|---|
Financial budget (max_budget, team/user budgets) | Requests when recorded + reserved spend would exceed a dollar cap | Virtual key, team, user, or global litellm_settings.max_budget | ExceededBudget / ExceededTokenBudget / budget_exceeded |
Rate limit (rpm_limit, tpm_limit) | Throughput (requests or tokens per minute) | Key, team, user, model | rate_limit / TPM or RPM messages — not dollar spend |
| Soft budget / alerts | Nothing by itself — notifications only | Callbacks, dashboards, external monitoring | Alert fired but requests still return 200 |
A failed request alone does not prove budget enforcement. Match the error shape and correlate with /key/info spend.
Where should I configure the budget?
For most applications, put the cap on the virtual key the service uses for inference — not the proxy master_key.
| Scope | When to use | Enforced against |
|---|---|---|
Virtual key (/key/generate → max_budget) | One app, CI job, or environment | That key’s spend |
Team (team_id on the key) | Shared pool across teammates | Team budget; key-owner personal budget does not apply |
Internal user (user_id on the key) | Multiple keys per engineer | User-level spend across their keys |
Global proxy (litellm_settings.max_budget) | Whole-deployment ceiling | All traffic on the proxy |
End-user / customer (user field + max_end_user_budget_id) | SaaS metering per customer | Customer id across keys |
Team scope details: LiteLLM proxy users .
Requirements (documented): budgets need Postgres. Without a database, virtual keys cannot resolve and no key/team/user budget is enforced. Global max_budget fails open without a DB. See LiteLLM budget docs .
Redis: budget reservation and cross-pod spend counters use Redis in multi-worker deployments. See budget reservation . A single local container can run with Redis for realistic concurrency behavior.
How do I create a budgeted virtual key and inspect its spend?
- Deploy the proxy with Postgres — see proxy setup.
- Generate a dedicated test key (keep the master key admin-only):
curl -X POST 'http://localhost:4000/key/generate' \
-H 'Authorization: Bearer sk-<master-key>' \
-H 'Content-Type: application/json' \
-d '{
"max_budget": 0.01,
"budget_duration": "30d",
"models": ["gpt-4o-mini"],
"key_alias": "app-dev",
"metadata": {"env": "dev"}
}'- Inspect spend and limits before and after test traffic:
curl 'http://localhost:4000/key/info?key=sk-<virtual-key>' \
-H 'Authorization: Bearer sk-<master-key>'| Field | Meaning |
|---|---|
max_budget | Dollar cap for the current budget window |
spend | Recorded spend LiteLLM uses for enforcement |
budget_duration | Reset window (30d, 24h, 30s, …) |
budget_reset_at | Next scheduled reset (when duration is set) |
models | Allowlist — requests to other models fail before budget is checked |
On successful completions, check x-litellm-response-cost in response headers and aggregate reports:
curl 'http://localhost:4000/global/spend/report?start_date=2026-01-01&end_date=2026-01-31&group_by=team' \
-H 'Authorization: Bearer sk-<master-key>'Tag requests for cost attribution
Pass tags in the request body for feature-level slicing (does not replace key budgets):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hello"}],
user="jane_smith",
extra_body={"metadata": {"tags": ["feature:code-review", "env:prod"]}},
)Warning: Clients can set user freely. For backend services, issue per-user virtual keys with user_id at creation — do not rely on self-declared user for enforcement.
How do I verify that LiteLLM rejects a request because of the budget?
Central question: how do I prove this budget is enforced, rather than merely displayed?
Evidence that counts
| Check | Pass signal |
|---|---|
| HTTP status + JSON body | ExceededTokenBudget, ExceededBudget, or error.type: "budget_exceeded" |
/key/info before/after | spend at or above max_budget (or admin lowered max_budget below spend) |
| Proxy logs | Budget check logged on the rejecting worker |
| Not sufficient | Generic 401, provider 429, or upstream 5xx without budget fields |
Example rejection shapes from virtual key budgets :
{
"detail": "Authentication Error, ExceededTokenBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07"
}{
"error": {
"message": "ExceededBudget: Key over budget. Spend=0.01, Budget=0.001",
"type": "budget_exceeded",
"code": "400"
}
}Minimal manual proof (one allowed, one blocked)
Do not burn a large production budget to test blocking. Either:
- Use a tiny test key on a disposable proxy, or
- Lower
max_budgetbelow recordedspendwith/key/updateto simulate exhaustion without extra provider traffic.
# After at least one successful request recorded spend:
curl -X POST 'http://localhost:4000/key/update' \
-H 'Authorization: Bearer sk-<master-key>' \
-H 'Content-Type: application/json' \
-d '{"key": "sk-<virtual-key>", "max_budget": 0.000001}'
curl -X POST 'http://localhost:4000/chat/completions' \
-H 'Authorization: Bearer sk-<virtual-key>' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "test"}],
"max_tokens": 16,
"stream": false
}'That proves the already-exhausted path. Use the verification script for concurrent near-limit behavior.
What changes when requests run concurrently?
LiteLLM 1.100.0 enables budget reservation by default. See budget reservation :
- Estimate max cost from the request body (
max_tokens) and model pricing. - Reserve that amount against the budget before calling the provider.
- Reject early if the reservation would exceed the cap.
- Replace the reservation with actual cost after the response is priced.
| Term | Meaning |
|---|---|
| Recorded spend | Value in Postgres / /key/info after reconciliation |
| Reservation | Temporary hold for in-flight requests |
| Final request cost | Actual priced tokens (may differ from the estimate) |
Near the limit, some concurrent workers succeed and others return budget_exceeded — order is not guaranteed. A single concurrency test does not prove zero overspend under all load patterns.
general_settings.disable_budget_reservation: true removes pre-request holds; concurrent requests can exceed the cap until spend is recorded (config ). LiteLLM logs a warning while this is enabled.
Why might requests be blocked before reported spend reaches the limit?
| Cause | What to inspect | Fix |
|---|---|---|
| Reservation > actual | Request omits max_tokens; proxy uses a large default estimate | Set explicit max_tokens / max_completion_tokens on client requests |
| Team or user budget | Key has team_id — team budget applies, not key max_budget alone | Check /team/info, team-member max_budget_in_team |
| Model allowlist | Error before budget check | Align models on the key with the requested model name |
| Per-model budget (Enterprise) | /key/info → model_max_budget_usage | Enterprise per-model caps (docs ) |
| Multiple budget windows | budget_limits with daily + monthly caps | Inspect each window in /key/info |
| Stale reservation | Requests fail after earlier calls completed | Wait for reconciliation; avoid disable_budget_reservation unless mitigating a known issue |
Why might requests continue when I expected them to stop?
| Cause | What to inspect | Fix |
|---|---|---|
| No database | Startup warning; keys not resolved | Add Postgres database_url — proxy setup |
| Bypassing the proxy | App calls provider directly | Route all inference through the proxy URL |
| Zero-cost model | input_cost_per_token and output_cost_per_token both explicitly 0 | Budget checks are skipped for zero-cost models (docs ) |
| Unpriced / wrong model map | Cost falls back to 0 for unknown models (regression fixed in recent releases) | Pin model names; set explicit model_info pricing |
| Redis counter stale after restart | Spend in DB higher than Redis hot counter | Enable fail_closed_budget_enforcement for hard ceilings (docs ) |
| Reservation disabled | disable_budget_reservation: true | Re-enable reservation for concurrent safety |
| Wrong key | Multiple keys in env | Confirm the app’s Authorization bearer matches the budgeted key |
How do I restore access after a legitimate budget reset or adjustment?
Normal recovery — raise the cap or wait for the reset window, not disable enforcement:
# Temporary increase (documented on virtual keys)
curl -X POST 'http://localhost:4000/key/update' \
-H 'Authorization: Bearer sk-<master-key>' \
-H 'Content-Type: application/json' \
-d '{
"key": "sk-<virtual-key>",
"max_budget": 5.0,
"temp_budget_increase": 2.0,
"temp_budget_expiry": "7d"
}'Check budget_reset_at in /key/info when budget_duration is set. Scheduled resets run on an internal scheduler (default check interval ~10 minutes; tunable via proxy_budget_rescheduler_min_time / max_time — reset docs ).
Admin spend reset endpoints exist for correcting accounting errors; prefer budget adjustment for routine recovery. After a reset, verify with one allowed request and /key/info.
Troubleshooting: budget not working
| Symptom | Evidence to inspect | Likely cause | Fix | Verification |
|---|---|---|---|---|
| Spend shows in UI but requests never block | /key/info spend > max_budget, requests still 200 | No DB on enforcement path, zero-cost model, or bypassing proxy | Add Postgres; confirm non-zero pricing; route all traffic through proxy | Lower max_budget below spend; expect budget_exceeded |
401 Authentication Error only | Body lacks ExceededBudget / ExceededTokenBudget | Wrong or expired key | Regenerate key; fix client env | Valid key returns 200; invalid returns 401 without budget fields |
budget_exceeded but spend looks low | Concurrent requests; reservations | Reservation holds near limit | Set explicit max_tokens; retest sequentially | Sequential requests succeed until true spend exceeds cap |
Blocked below max_budget | Debug logs mention reservation | max_tokens estimate too high | Reduce max_tokens or raise budget slightly | Request with smaller max_tokens succeeds |
| Team key ignores personal cap | Key has team_id | Team budget scope (v1.95.0+ ) | Set max_budget on team or max_budget_in_team | /team/info spend matches enforcement |
| Works on one pod, drifts on cluster | Multi-replica deploy without Redis | Per-pod spend drift | Add Redis; consider fail_closed_budget_enforcement | Concurrent test from verification example |
| Provider bill >> LiteLLM spend | Compare model names and dates | Wrong base_model / cost map | Set model_info.base_model and custom pricing | x-litellm-response-cost matches expected per request |
Enforcement limits (LiteLLM 1.100.0)
Documented behavior (sources ):
- Budget checks read spend from the database; Redis holds the hot cross-pod counter.
- Reservation is on by default for chat completions with token pricing.
fail_closed_budget_enforcement(off by default) rejects with503when spend cannot be verified in Redis or DB.- Batch jobs (
POST /batches) cannot be fully reserved at submission — monitor completed batch cost separately. - Streaming, images, audio, unpriced models — not covered by the walkthrough below; verify separately before trusting caps.
Observed in the local verification example (network_mock, pinned image): virtual-key max_budget blocks chat completions with structured budget_exceeded / ExceededTokenBudget errors; concurrent near-limit admission matches reservation semantics above.
Not proven by mocks: network_mock exercises proxy accounting, not your provider’s invoice. Re-run against your real model before production sign-off.
Run the budget verification example
The repo ships a bounded, opt-in script with a disposable Docker Compose stack. Default mode uses network_mock — no paid provider calls.
| Pin | Value |
|---|---|
| LiteLLM image | docker.litellm.ai/berriai/litellm:main-v1.100.0-stable (1.100.0) |
| Database | Postgres 16 |
| Cache / reservation | Redis 7 |
| Model | budget-test-model (input_cost_per_token: 0.000001, output_cost_per_token: 0.000002) |
| Request style | Non-streaming POST /chat/completions with max_tokens: 32 |
Files: public/examples/litellm-budget-verify/ in the wiki repo .
cd public/examples/litellm-budget-verify
docker compose up -d
export LITELLM_RUN_LIVE=1
# optional: LITELLM_BASE_URL=http://localhost:4000
# optional: LITELLM_MASTER_KEY=sk-litellm-budget-verify-master
node verify.mjs
docker compose down -v # cleanupDownload: verify.mjs · helpers.mjs · docker-compose.yml · config.yaml
Safety caps (enforced in script)
| Cap | Default |
|---|---|
| Inference requests | ≤ 10 |
| Concurrent workers | ≤ 3 |
| Retries | 0 (num_retries: 0 in config) |
| Paid provider | Disabled unless you modify the example (not recommended) |
| Opt-in | LITELLM_RUN_LIVE=1 required |
Preflight prints a conservative cost estimate from token pricing assumptions. The script refuses to run without LITELLM_RUN_LIVE=1. CI and pnpm check never call LiteLLM or paid APIs.
What the four cases exercise
| Case | Action | Expected |
|---|---|---|
| A. Allowed | Create key, inspect /key/info, one request with headroom | 200, spend increases, x-litellm-response-cost present |
| B. Rejected | Admin lowers max_budget below recorded spend | budget_exceeded / ExceededTokenBudget — already-exhausted path |
| C. Concurrent | Fresh key with ~1.4× single-request budget; 3 parallel workers | Mix of accept/reject; final spend reconciled |
| D. Recovery | /key/update raises max_budget | Next request succeeds |
Offline unit tests in lib/litellm-budget-verify.test.ts cover error classification and cost parsing only — they do not prove live proxy enforcement.
Wiring your own provider (optional)
To test against a real model, copy the example to an isolated proxy, set provider credentials via environment variables, remove network_mock, and keep the same safety caps. Expect provider charges; the stock example does not enable this path.
Spending guardrails (production)
- Never share the master key with application code
- Set
max_budgeton every virtual key; usemodelsallowlist on CI keys - Set explicit
max_tokensso reservations match real usage - Export weekly spend via
/global/spend/report - Sync model pricing when provider rates change — cost tracking
Related
- LiteLLM hub
- Proxy setup — Postgres, client
baseURL, master key - Model routing — fallbacks and allowlists
- Budgets, rate limits (official)
- Proxy config settings
- Spend tracking