LiteLLM budget per key: what it enforces and where it ends
A LiteLLM key budget is a max_budget in USD on a virtual key, with an optional reset window. Per LiteLLM's documentation, read on 14 October 2026, the proxy reserves the worst-case cost before sending a request and rejects it if the budget would be crossed. The same pages say budgets need a database, can be bypassed by zero-cost models, and trust a stale Redis counter unless a fail-closed flag is set.
A LiteLLM key budget is a USD amount on a virtual key
LiteLLM's proxy issues virtual keys with the master key and attaches a budget to them. The budgets page, read on 14 October 2026, gives this call:
curl 'http://0.0.0.0:4000/key/generate' \
--header 'Authorization: Bearer <your-master-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"max_budget": 10,
"budget_duration": "30d"
}'
The page states the expected behaviour in three lines: costs per key are recorded in a table, requests fail once the key crosses its max_budget, and "If duration set, spend is reset at the end of the duration". It adds: "By default the max_budget is set to null and is not checked for keys".
Spend is recorded in US dollars. The virtual keys page says "Spend is automatically tracked for the key in the LiteLLM_VerificationTokenTable", and for a key with a user_id or team_id also against that user and team.
Budgets exist at several levels, and the page lists them:
| Level | How it is set | Note from the page |
|---|---|---|
| Key | max_budget on /key/generate | Personal budget when the key has no team |
| Team | max_budget on the team, team_id on the key | "If a key belongs to a team, only the team (and team-member) budgets are enforced" |
| Several windows | budget_limits: a list of {budget_duration, max_budget} | Each window "resets on its own schedule" |
| Per model on a key | model_max_budget | "This feature requires a LiteLLM Enterprise license" |
| Agent session | max_budget_per_session in litellm_params | Over the limit, "requests receive a 429 Too Many Requests response" |
The duration units are seconds, minutes, hours and days (30s, 30m, 30h, 30d). The multi-window table adds the reset times: 24h resets "Daily at midnight UTC", 7d every Monday, 30d on the 1st of the month. The page also notes that "By default, the server checks for resets every 10 minutes, to minimize DB calls", so the reset is applied on that cadence, not at the exact boundary, unless the rescheduler settings are changed.
A per-agent budget is, in practice, a key per agent with its own max_budget. The daily spending limit guide compares how the providers' own caps and LiteLLM budgets reset.
The budget is checked before the call, against a worst-case estimate
Where it can price a request, LiteLLM does not wait for spend to be recorded. The budgets page describes "Budget reservation", enabled by default:
LiteLLM estimates the request's maximum cost from the request body and the model's pricing. It temporarily reserves that amount against the applicable budget. If the reservation would exceed the budget, LiteLLM rejects the request before sending it to the provider. After the response is priced, LiteLLM replaces the reservation with the actual cost.
The estimate uses max_tokens or max_completion_tokens when the request carries one, "Otherwise, it uses the model's configured limits". That makes the output bound a property of the request the agent builds. An agent that omits max_tokens is estimated from the model's configured limits instead.
Two documented gaps sit in this mechanism. Some routes have no token pricing; for "some image and audio routes, LiteLLM cannot reserve a cost and instead enforces the budget using recorded spend". A batch submission contains a file id rather than prompts, so "LiteLLM cannot price the complete workload at submission" and records the final cost when the batch completes. For both, the cost is recorded later, so what is in flight is not yet counted against the budget. The Cloudflare entry covers a gateway where that after-the-fact recording is the whole mechanism.
Disabling reservation (disable_budget_reservation: true) is documented as a temporary mitigation if unreconciled reservations cause unexpected refusals, and the page warns that it lets concurrent requests exceed a configured budget.
Three documented ways a budget stops capping
The same page lists conditions under which a configured number does not bound spend. Each is quoted here with the setting that closes it.
No database. The page says: "Every budget on this page is enforced against spend read from the database, so none of them cap anything on a DB-less deployment." The global budget in litellm_settings.max_budget "fails open there rather than erroring", with a warning logged once at startup and "nothing blocks at request time". Key, team and user budgets are unavailable too, because virtual keys cannot be resolved without a database.
A stale counter. Budget checks read a Redis counter, with the database reconciled in the background. If Redis reloads an older snapshot, "on the hot path that stale value is trusted, which can let a key keep spending past its max_budget until the counter is corrected". The closing setting is documented:
general_settings:
fail_closed_budget_enforcement: true
With it on, every budgeted request validates spend against the database, and "If current spend can be verified against neither Redis nor the database, the request is rejected with a 503 instead of being admitted on an unverifiable budget". It is off by default, and the page describes the cost as bounded extra reads.
Zero-cost models. "Budget checks are skipped entirely for zero-cost models." A model counts as free only when both input_cost_per_token and output_cost_per_token are explicitly 0; if they are unset, "the model is not treated as free and budget checks still apply". This is a feature for local fallback models. It also means a pricing entry set to zero is an unmetered path for any key.
The documentation states all three. They are defaults to check on a deployment where the budget is meant to be a ceiling. The page's example of a crossed key budget returns this body, which is what a client sees when the cap works:
{ "detail":"Authentication Error, ExceededTokenBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07" }
Whoever holds the master key sets the number, and keys are bearer strings
A LiteLLM budget is configuration. It is a field on a key, user or team record, set at generation and changeable through the key edit endpoints that the page names. The master key is, in the page's words, "your Proxy Admin key". The virtual keys page shows a key as Authorization: Bearer <generated-key>: whoever presents the string is the key.
The page is candid about edit paths. Its warning on custom_key_generate reads: "Key edits (/key/update, /key/bulk_update, /team/key/bulk_update, and editing a key in the Admin UI, which calls /key/update) skip it, so a user can create a compliant key and then edit it out of compliance". Operators can set upperbound_key_generate_params to cap max_budget and budget_duration for generated keys, and custom_key_policy to run on every key operation. There is also a documented temp_budget_increase with a UTC expiry for team members.
None of this is a defect. A proxy needs an admin who can raise a budget. The point for an agent fleet is who that admin is and what the record shows afterwards. The pages read describe the key's spend counters; they do not describe a signature over the budget itself or a tamper-evident record of changes to it. If an agent's host also holds the master key, or the database, the budget is whatever the holder says it is. The stolen-credential entry covers what changes when requests are signed under a key rather than presented as a string.
Where LiteLLM is the better choice
On breadth, LiteLLM is the more complete product, and the Mandare door is not a substitute for it.
- Reach. One proxy fronts many providers and models, with routing and fallbacks (the page documents
router_settings.fallbacks). The Mandare door proxies two native routes. - Organisational structure. Keys, users, teams, customers and tags each carry budgets, and a UI manages them.
- Windows. Several concurrent budget windows on one key,
budget_limitsof24hand30dfor example, are documented. The Mandare mandate caps per transaction, per day, per task and in total. - Per-model and per-session caps. Enterprise per-model key budgets and agent session budgets exist in LiteLLM. The Mandare CLI has no per-model budget flag.
- Zero-cost fallback. A budget-exhausted key can still reach a local model. The Mandare door refuses.
- Maturity. LiteLLM has a UI, a changelog and a large documented surface. Mandare is v0.1.0.
Where a signed mandate fits, and where it stops
Mandare is an open-source control layer. Its gateway, the door, enforces a mandate that the owner signs once, and records every call and refusal in a hash-chained ledger. This compares the two on the points above (Mandare as of v0.1.0, the version npm view @mandarelabs/cli version printed on 14 October 2026):
| Question | LiteLLM (pages read 14 October 2026) | Mandare door (v0.1.0) |
|---|---|---|
| Where do the caps live? | Fields on key, user and team records in the proxy database | Fields inside a signed credential (SD-JWT VC); the concepts page calls mandates "signed by the owner and self-verifying" |
| When is a call counted? | Worst-case reservation before the call, replaced by actual cost | Estimated cost reserved inside the ledger transaction before the call; true cost settles after |
| Who is the budget attached to? | A bearer string | In passport mode, an agent key; requests are RFC 9421 signed under it |
| Model with no price? | Not stated for unpriced models; zero-cost models skip checks | Refused with MODEL_UNPRICED; a refusal is a ledger entry |
| If the ledger or database is unavailable | Global budget fails open without a database; opt-in fail_closed_budget_enforcement | "ledger down → no action", per the README |
| Day boundary | 24h resets at midnight UTC, checked about every 10 minutes | UTC day bucket (packages/ledger/src/projection.ts) |
The demo, pnpm demo, runs a loop against a €20 per day mandate with a mock provider. This is the capture in docs/demos/S2-runaway-demo.txt, cut with …:
…
call # 72 403 DENIED — THE LOOP DIES HERE
code: PER_DAY_EXCEEDED
reason: per-day budget exceeded: reserved 0.00 EUR + settled 19.723587 EUR + estimate 0.278853 EUR > cap 20.00 EUR
the refusal itself is ledger entry 59034994bfacad09…
[demo] runaway made 71 calls in 0.1s before the mandate killed it.The limits, stated next to the claims they limit:
- The cap holds given a correct price table; the environment reference says an unlisted model variant is "refused, never priced as a sibling", so the operator extends the table. We make no claim that a cap cannot be exceeded under any condition.
- The door proxies Anthropic
POST /v1/messagesand OpenAI or OpenRouterPOST /v1/chat/completions, as the providers page lists. It does not route across providers. - No example or test in the repository places the door in front of, or behind, a LiteLLM proxy, so the two are not claimed to work together.
- Keys in
.envbehind an unauthenticated door are, in the quickstart's words, "a starting point, not a boundary". A budget in either product holds against an agent that has no other path to the provider key. - The demo is a mock: the enforcement is real, the money is not. The card rail has been tested against a Stripe mock; the live test-mode run has not happened.
- There is no hosted service. The release has had two AI-assisted review passes and no external audit.
Questions
How is a budget set per key in LiteLLM?
Call /key/generate with the master key and a max_budget in USD, for example 10, and add budget_duration such as 30d for a reset. Without a duration the budget never resets. Per the page read on 14 October 2026, a null max_budget is not checked at all, and every budget needs a database behind the proxy.
What error does LiteLLM return when a key exceeds its budget?
The documented body is Authentication Error, ExceededTokenBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07. A team member over the team budget gets ExceededBudget: Crossed spend within team. The pages do not list a status code next to the key error, so match on the text rather than assuming one.
Can a LiteLLM budget be exceeded?
Yes, in three documented cases. Without a database, the global budget fails open. If Redis reloads an older snapshot, the stale counter is trusted by default. Models whose cost fields are explicitly set to 0 skip budget checks. Setting fail_closed_budget_enforcement: true closes the Redis case and returns a 503 when spend cannot be checked.
Does LiteLLM have a per-agent budget?
Per-key budgets are the usual route, with a key per agent. LiteLLM also documents agents registered with its Agent Gateway, which take max_iterations and max_budget_per_session in litellm_params. A session over the limit gets a 429. That is a per-session cap, not a daily or total cap on the agent.
Sources
Run it yourself: 3 commands, no API keys.
github.com/mandarelabs/mandare →