Skip to content

Cap what a team spends

Set a budget on a profile to cap traffic in a fixed window. By default, each key on the profile receives its own allowance.

Declare the budget and attach it to the profile:

budgets:
- name: research-daily
limit: 2000000
window: day
cost: {type: TotalToken}
profiles:
- name: research
budget: research-daily

This profile receives two million tokens each day. A profile without a budget is unmetered.

limit counts cost units, and cost says what one request draws. There is no price table and no currency anywhere in the configuration, because the gateway does not know what you pay.

TotalToken is the default and the right answer for most deployments. Reach past it when you are rationing something narrower. Use OutputToken when generation is what costs, or ReasoningToken to cap the expensive mode without touching ordinary traffic. Use type: CEL when different models have to be weighted against each other. That is the only way to express a price. See Metering for the full type list and the expression’s vocabulary.

window is minute, hour, day, week or a Go duration of at least a minute, and it defaults to day.

Windows are fixed and aligned in UTC, not measured from when you declared them. A day budget resets at 00:00 UTC, and a week starts on a Monday. A duration is aligned the same way, so 12h means 00:00 and 12:00 UTC and not twelve hours from now.

Choose whether keys share one allowance before you deploy the budget.

budgets:
- name: research-daily
limit: 2000000
window: day
shared: true # one bucket for the whole profile

The default is shared: false. Each key gets its own limit, so ten keys receive ten separate two-million-token allowances. Use it for per-service keys, not for one team-wide allowance.

shared: true gives the profile one bucket that every key draws from. One noisy client can then exhaust the team. Neither option is safer. They answer different questions.

Sharing also decides what a key rotation costs you. A per-key bucket is keyed by the key’s name, so a key rotated mid-window starts with a full allowance again. A shared bucket does not reset for a new name. See Mint, rotate and revoke a virtual key.

List the active budget buckets:

Terminal window
$ pistra admin /admin/v1/budgets

Use scope to distinguish per-key and shared buckets:

{"budgets":[
{"budget":"research-daily","scope":"alice","window_start":"2026-09-01T00:00:00Z",
"used":418233,"limit":2000000,"remaining":1581767,"status":"live"},
{"budget":"research-daily","scope":"bob","window_start":"2026-09-01T00:00:00Z",
"used":12,"limit":2000000,"remaining":1999988,"status":"live"},
{"budget":"contractors-daily","limit":500000,"remaining":500000,"status":"idle"}
]}

scope is the key name on a per-key bucket and absent on a shared one. That is the quickest way to confirm you got the shared decision you meant. idle is a declared budget nothing has drawn on yet. The list is the limits in force, not only the ones in use.

A third status, draining, is a bucket whose budget the configuration no longer declares. Those are kept and shown deliberately. Dropping them would hide spend that really happened, and a budget view must not do that.

Clients receive this response when a budget is exhausted:

429 budget_exceeded
budget "research-daily" exhausted: 1204 of 2000000 cost units remaining,
request needs 4096

The message quotes the arithmetic because the alternative is a client retrying against a limit it cannot see. The refusal is recorded as a request.budget audit event naming the budget rather than only the key. That makes “which limit stopped them” answerable without guessing from timing.

pistra_budget_used_ratio{budget} is the one to watch, and the threshold goes below 1. Refusals only start once somebody tries, so alerting on the 429 is finding out from your users. It reports the fullest of the budget’s buckets rather than an average, because a budget with per-key allowances has no single fill level.

Pair it with pistra_budget_buckets{budget,state}. The ratio alone cannot tell one exhausted key from every exhausted key, since both read 1.0. → Watch the gateway

It is a cluster limit, but not a hard one. Nodes flush spend to each other every 500 ms, so a limit can be overrun by roughly what the rest of the cluster can spend in half a second. It cannot be overrun by a multiple of itself.

Cost is held before the request and settled after it. The hold is a guess, body length over four for input, max_tokens or 1024 for output. Settlement replaces it with what the provider reported. A provider that reports nothing settles at the guess rather than at zero.

Both are deliberate trades, and Metering argues each one.