Contents
Figure 1: LiteLLM Virtual Keys and Budgets: Control Who Spends What — a ceiling per key, a ceiling per team, and a story for when both fail
Someone on your team leaves a script running over the weekend. By Monday, it has spent more than the whole month's allowance. Who caught it, and when?
LiteLLM's virtual keys and budgets exist to answer that question before it hurts. This guide explains how LiteLLM virtual keys and budgets work, which budget wins when several apply, and where the sharp edges hide. I drew every behaviour from the official budget docs and checked each claim against them while writing this.
Start Here: Budgets Need a Database
This rule trips up more people than any other. The docs state that every budget is enforced against spend read from the database. On a deployment without one, none of them cap anything [1].
Worse, the failure is silent. A max_budget set in litellm_settings fails open without a database, so requests keep flowing past the limit. The proxy logs one warning at startup and nothing at request time. Virtual keys fail too, with a No connected db. error [1].
So run Postgres before you design any budget policy. My earlier LiteLLM guide covers the Docker Compose setup that includes one, and the team-sharing guide covers who should hold the master key.
The Budget Levels and Which One Wins
LiteLLM lets you set budgets at several levels. The official page lists these setup options [1]:
| Level | What it caps |
|---|---|
| Global proxy budget | One ceiling across all calls on the gateway |
| Personal budget | Keys created without a team_id |
| Team budget | Keys carrying a team_id, drawn from one shared pot |
| Team member budget | Individual limits inside a team's shared budget |
| Key budget | A single virtual key |
| Customer budget | The user value your app passes in requests |
Here is the rule that matters most. If a key belongs to a team, only the team and team-member budgets are enforced, and the key owner's personal budget does not apply [1]. The docs add a history note: one release briefly enforced both, and a later release removed that behaviour [1]. If you see old advice that says otherwise, trust the current page.
Key Budgets: The Simplest Control
A key budget caps what one virtual key can spend. Set max_budget when you create the key, and requests fail once the key crosses it [1].
curl 'http://localhost:4000/key/generate' \
--header 'Authorization: Bearer <your-master-key>' \
--header 'Content-Type: application/json' \
--data-raw '{"max_budget": 10, "budget_duration": "30d"}'
The budget_duration field controls resets. The docs say that if you do not set it, the budget never resets, so a $10 budget means $10 forever [1]. You can write the duration in seconds, minutes, hours, or days, such as 30s, 30m, 30h, or 30d.
By default, max_budget is null and the proxy does not check it on keys. A key with no budget has no cap at all [1].
Stack Several Budget Windows on One Key
A single monthly cap cannot stop one bad day from eating the month. The docs solve that with multiple budget windows, using the budget_limits list [1].
curl 'http://localhost:4000/key/generate' \
--header 'Authorization: Bearer <your-master-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"budget_limits": [
{"budget_duration": "24h", "max_budget": 10},
{"budget_duration": "30d", "max_budget": 100}
]
}'
This key can spend $10 a day and $100 a month. The windows track independently, and each resets on its own schedule [1]:
| Window | Resets |
|---|---|
1h |
Every hour |
24h |
Daily at midnight UTC |
7d |
Every Monday at midnight UTC |
30d |
The 1st of every month at midnight UTC |
That table describes the default. A timezone setting moves those midnights off UTC, and the reset time of day is configurable [3]. For coding assistants and agents, where one runaway session can burn serious money, this is the single most useful budget feature in the product.
Team Budgets and Team Member Budgets
Teams give you shared pots of money. Create a team, optionally with a budget, rate limits, and a reset schedule [2].
curl 'http://localhost:4000/team/new' \
--header 'Authorization: Bearer <your-master-key>' \
--header 'Content-Type: application/json' \
--data-raw '{"team_alias": "research", "max_budget": 500, "budget_duration": "30d"}'
A team budget alone has a weakness: one member can consume it all. Team member budgets fix that. Add a user to the team with max_budget_in_team, then create their key with both user_id and team_id [2]:
curl -X POST 'http://localhost:4000/team/member_add' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{"team_id": "<team-id>", "max_budget_in_team": 25,
"member": {"role": "user", "user_id": "alice"}}'
When Alice crosses her limit, the proxy returns an ExceededBudget: Crossed spend within team error [2]. Her teammates keep working.
Change a Member's Budget Without Surprises
The docs describe several behaviours worth knowing before you adjust limits [2]:
- Updates take effect on the next request. Use
/team/member_updatewith a newmax_budget_in_team. - Changes are permanent. A raised budget stays raised in every later cycle, and resets only set current-cycle spend back to zero.
- Temporary increases exist. Send
temp_budget_increasewithtemp_budget_expiryas a UTC datetime, and the extra amount stops applying at that time without any cleanup. - Old spend counts against new budgets. If a member spent $500 with no budget and you then set a $100 limit, their next request fails. A budget with a
budget_durationclears itself at the next reset, and one without it stays blocked until you reset the spend. - A member with no budget has no window at all, so their current-cycle spend is never reset automatically and keeps growing.
Only a proxy admin can reset a member's spend, through the UI or the reset_spend endpoint; members do not see that control on their own row [2]. That fourth point catches admins by surprise. Adding a budget later does not start fresh.
Budgets for Your Customers, Not Just Your Team
Building a product on top of the gateway? You may need limits per end customer without creating a key for each one. LiteLLM budgets the user field that your app passes to /chat/completions [1].
Create a budget, then point your config at it:
litellm_settings:
max_end_user_budget_id: "default-customer-budget"
This budget applies to every customer without their own, including customers who do not exist in the database yet [1]. Two details from the docs: the proxy caches the budget for 60 seconds, so edits take up to a minute to apply, and the older max_end_user_budget float setting no longer enforces anything [1]. Customer budgets are also global per deployment, so one customer shares a single budget across every key and team.
Defaults for Every Internal User
Want a safety net for everyone you give keys to? Set a default budget in the config [1]:
litellm_settings:
max_internal_user_budget: 10
internal_user_budget_duration: "1mo"
This applies to users with the internal_user role. It does not apply to keys that carry a team_id, because team budgets take over there [1].
Enterprise-Only: Per-Model Budgets
The docs describe model_max_budget, which lets one key carry a separate budget for each model, such as a daily limit on an expensive model and a monthly limit on a cheap one. They also describe per-model budgets on internal users, which stop a person from dodging a limit by creating more keys. Both are marked LiteLLM Enterprise features that need a licence [1].
The /key/info endpoint then reports model_max_budget_usage, which shows current spend per budgeted model for the active period [1]. By default, an exceeded per-model budget returns a budget_exceeded error, and the docs point to budget fallbacks if you would rather route to another model [5].
Rate Limits: The Other Half of Control
Budgets cap dollars, and rate limits cap speed. You can set tpm_limit (tokens per minute), rpm_limit (requests per minute), and max_parallel_requests on keys, users, and teams [1].
curl --location 'http://localhost:4000/key/generate' \
--header 'Authorization: Bearer <your-master-key>' \
--header 'Content-Type: application/json' \
--data '{"max_parallel_requests": 10, "tpm_limit": 20000, "rpm_limit": 60}'
Several details from the docs deserve attention:
| Setting | Behaviour |
|---|---|
token_rate_limit_type |
TPM counts total tokens by default; set input or output to change that |
model_rpm_limit / model_tpm_limit |
Per-model limits on keys or teams, verifiable from x-litellm-key-remaining-requests-<model> headers |
disable_fallbacks_on_per_model_rate_limits |
Turns per-model limits into hard caps that return 429 instead of retrying elsewhere |
default_estimated_output_tokens |
Tunes the output-size guess when a request omits max_tokens |
Limits apply regardless of role. The master key has no limits unless you configure them, so test with a virtual key that has explicit limits [1]. Missing max_tokens complicates TPM, because the proxy must reserve tokens for output it cannot see yet [1].
How Enforcement Really Works
The budget system has a few mechanics that explain its occasional surprises.
Reservation Before the Call
Budget reservation is on by default [1]. The proxy estimates a request's maximum cost from its body and the model's pricing, reserves that amount, and rejects the request up front if the reservation would exceed the budget. After the response, it swaps the reservation for the real cost. A max_tokens value in your request tightens the estimate.
This helps with concurrent traffic, but it also means a request can fail even when your remaining budget exceeds the typical cost. Batch jobs escape it, since the proxy cannot price a batch at submission [1].
Resets Are Not Instant
By default, the server checks for budget resets every 10 minutes to limit database calls [1]. You can tune that with proxy_budget_rescheduler_min_time and proxy_budget_rescheduler_max_time, so a budget that should reset at midnight may unblock a few minutes later.
Redis and Hard Limits
Budget checks read spend from a cross-instance counter in Redis, with the database reconciled in the background [4]. If Redis restarts with an old snapshot, the counter can read low, and a key may overspend until it corrects. For hard ceilings, the docs offer fail_closed_budget_enforcement: true, which validates spend against the database before admitting each budgeted request. If the proxy cannot verify spend, it rejects the request with a 503 [1].
Rate limits have a similar switch, fail_closed_rate_limit_enforcement. Without Redis across several instances, each instance counts on its own, so a limit of 2 requests per minute can become 2 per instance [4]. The trade-off is availability: fail-closed settings reject traffic when verification breaks.
A Free-Model Escape Hatch
When a key, user, or team exhausts its budget, requests to zero-cost models still pass. Budget checks skip them entirely [1]. To mark a self-hosted model as free, set both input_cost_per_token and output_cost_per_token explicitly to 0 in its config. Leaving them unset does not count [1].
A Starter Policy You Can Adapt
These figures are my suggestions, not defaults from the docs. Tune them to your costs.
| Key type | Budget | Reset | Rate limit | Why |
|---|---|---|---|---|
| Sandbox key | $5 | 7d | 30 rpm | Keeps curiosity cheap |
| Developer key in a team | $25 member budget | 30d | 60 rpm | Stops one person draining the team |
| Agent or coding-assistant key | $10 per 24h and $100 per 30d | Two windows | max_parallel_requests |
Blocks runaway sessions |
| Production service key | Team budget with alerts upstream | 30d | Sized to expected traffic | Predictable cost, separate tracking |
Test Your Budgets Before You Trust Them
Never assume a budget works. Prove it:
- Create a key with a tiny budget, like
0.0001. - Send two requests. The second should fail with a budget error.
- Check
/key/infofor the key's spend. - Test on the real deployment, with the real database, not a laptop with no database.
- Test resets with a short
budget_duration, and remember the 10-minute check interval.
Common Pitfalls
I have seen these cause real bills:
- Setting budgets without a database. The limits do nothing and warn only once.
- Forgetting
budget_duration. The budget never resets, so people get locked out or never capped. - Expecting a personal budget to apply on a team key. Only team and member budgets count.
- Adding a budget after heavy spending. Past spend counts, and the user is blocked immediately.
- Trusting one monthly cap. One bad day can empty it. Add a daily window.
- Skipping provider-side limits. A second cap catches proxy misconfigurations.
- Testing with the master key. It has no limits, so throttling looks broken.
- Marking a model free by omission. Both cost fields must be 0 explicitly, or budget checks still apply.
Best Book to Read
| Cover | Book | Description | Get it |
|---|---|---|---|
![]() |
Cloud FinOps | the discipline behind per-team cost ownership, budget alerts and showback, which is what a virtual-key budget is a small version of. | 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
Good spend control layers a few simple tools: a database, key and team budgets, member limits, multiple windows, and rate limits. Know which budget wins for a team key, remember that resets run on a schedule, and test every limit with a real key. Use the fail-closed settings when a budget must act as a hard ceiling.
So pick one key type from the starter table, create it with a tiny budget, and watch it get blocked. Seeing a budget actually fire builds more trust than reading any documentation, including this guide.
Frequently Asked Questions
Why does a LiteLLM max_budget not block any requests?
Two usual causes. First, no database: every budget is enforced against spend read from Postgres, so on a DB-less deployment max_budget fails open, requests keep flowing past the limit, and the proxy logs one warning at startup and nothing at request time. Second, no budget on the key at all: max_budget defaults to null, and a key with no budget has no cap. Fix the database first, then check /key/info for the key's actual max_budget and spend.
Which budget wins when a key belongs to a team?
The team and team-member budgets. If a key carries a team_id, the key owner's personal budget is not applied, so setting both does not give you two ceilings. The docs note that one release briefly enforced both behind a skip_user_budget_on_team_key opt-out and a later release removed both the behaviour and the flag, so old advice that says otherwise is out of date.
Can a LiteLLM key have both a daily and a monthly budget?
Yes, using budget_limits, a list of {budget_duration, max_budget} objects. Each window is tracked independently and resets on its own schedule, so a key can hold a 24h cap of 10 USD and a 30d cap of 100 USD at the same time. This is the practical defence against a single runaway session eating a monthly allowance in one afternoon.
When exactly do LiteLLM budgets reset?
By default 1h windows reset every hour, 24h windows reset daily at midnight UTC, 7d windows reset every Monday at midnight UTC, and 30d windows reset on the first of each month at midnight UTC. The server checks for resets every ten minutes by default to limit database calls, so a budget due at midnight can unblock a few minutes later. A timezone setting moves those midnights off UTC, and proxy_budget_rescheduler_min_time and max_time change the check interval.
What happens if I add a budget after a user has already spent money?
Past spend counts against the new budget immediately, so a member who spent 500 USD with no limit and then receives a 100 USD limit fails their next request. If that budget has a budget_duration, the block clears at the next reset. Without a duration it stays blocked until someone resets the spend, and only a proxy admin can do that.
Do budget-exhausted keys still reach free or self-hosted models?
Yes. Requests to models whose input_cost_per_token and output_cost_per_token are both explicitly 0 skip budget checks entirely, which makes a local model a working fallback for a depleted key. The keywords are explicitly: leaving both fields unset means the model is not treated as free and budget checks still apply.
Sources
[1]LiteLLM, Budgets, Rate Limits — the six budget levels, the rule that team keys ignore personal budgets with the 1.94.0/1.95.0 history note,budget_limitswith the per-window reset table,max_budgetdefaulting to null, the database requirement and fail-open behaviour, end-user and internal-user defaults,model_max_budget, budget reservation, the 10-minute rescheduler,fail_closed_budget_enforcement, zero-cost model exemptions, and the rate-limit settings: docs.litellm.ai/docs/proxy/users[2]LiteLLM, Setting Team Budgets —/team/newand/team/member_addwithmax_budget_in_team, theExceededBudget: Crossed spend within teamerror,/team/member_updateapplying on the next request,temp_budget_increaseandtemp_budget_expiry, the admin-onlyreset_spendendpoint, and the fact that past spend counts against a newly set budget: docs.litellm.ai/docs/proxy/team_budgets[3]LiteLLM, Budget Reset Times and Timezones — the default UTC reset schedule and the timezone and reset-time-of-day settings that move it: docs.litellm.ai/docs/proxy/budget_reset_and_tz[4]LiteLLM, Redis requirements — budget and rate-limit counters shared across instances, the per-instance fallback without Redis, andfail_closed_rate_limit_enforcement: docs.litellm.ai/docs/proxy/redis_requirements[5]LiteLLM, Budget fallbacks — routing a request to another model instead of returningbudget_exceeded: docs.litellm.ai/docs/proxy/budget_fallbacks
