Sam Austinon October 27, 202617 min read

LiteLLM Virtual Keys and Budgets: Control Who Spends What

LiteLLM Virtual Keys and Budgets: Control Who Spends What
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]:

  1. Updates take effect on the next request. Use /team/member_update with a new max_budget_in_team.
  2. Changes are permanent. A raised budget stays raised in every later cycle, and resets only set current-cycle spend back to zero.
  3. Temporary increases exist. Send temp_budget_increase with temp_budget_expiry as a UTC datetime, and the extra amount stops applying at that time without any cleanup.
  4. 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_duration clears itself at the next reset, and one without it stays blocked until you reset the spend.
  5. 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:

  1. Create a key with a tiny budget, like 0.0001.
  2. Send two requests. The second should fail with a budget error.
  3. Check /key/info for the key's spend.
  4. Test on the real deployment, with the real database, not a laptop with no database.
  5. Test resets with a short budget_duration, and remember the 10-minute check interval.

Common Pitfalls

I have seen these cause real bills:

  1. Setting budgets without a database. The limits do nothing and warn only once.
  2. Forgetting budget_duration. The budget never resets, so people get locked out or never capped.
  3. Expecting a personal budget to apply on a team key. Only team and member budgets count.
  4. Adding a budget after heavy spending. Past spend counts, and the user is blocked immediately.
  5. Trusting one monthly cap. One bad day can empty it. Add a daily window.
  6. Skipping provider-side limits. A second cap catches proxy misconfigurations.
  7. Testing with the master key. It has no limits, so throttling looks broken.
  8. Marking a model free by omission. Both cost fields must be 0 explicitly, or budget checks still apply.

Best Book to Read

CoverBookDescriptionGet it
Cover of “Cloud FinOps” Cloud FinOpsby J.R. Storment and Mike Fuller 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_limits with the per-window reset table, max_budget defaulting 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/new and /team/member_add with max_budget_in_team, the ExceededBudget: Crossed spend within team error, /team/member_update applying on the next request, temp_budget_increase and temp_budget_expiry, the admin-only reset_spend endpoint, 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, and fail_closed_rate_limit_enforcement: docs.litellm.ai/docs/proxy/redis_requirements
  • [5] LiteLLM, Budget fallbacks — routing a request to another model instead of returning budget_exceeded: docs.litellm.ai/docs/proxy/budget_fallbacks

What are You Looking For?

esc