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-dailyThis profile receives two million tokens each day. A profile without a
budget is unmetered.
Decide these three things
Section titled “Decide these three things”What a unit is
Section titled “What a unit is”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.
When it resets
Section titled “When it resets”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.
Whether the team shares one allowance
Section titled “Whether the team shares one allowance”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 profileThe 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.
Check what it is doing
Section titled “Check what it is doing”List the active budget buckets:
$ pistra admin /admin/v1/budgetsUse 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.
What a client sees when it runs out
Section titled “What a client sees when it runs out”Clients receive this response when a budget is exhausted:
429 budget_exceededbudget "research-daily" exhausted: 1204 of 2000000 cost units remaining,request needs 4096The 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.
Alert before that happens
Section titled “Alert before that happens”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
Two things a budget does not promise
Section titled “Two things a budget does not promise”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.
Related
Section titled “Related”- Metering, why the gateway never counts tokens itself, and everything a cost expression may name
- Give a team its own policy, the profile that names the budget
- Audit your metering, reconciling this ledger against the bill you were actually sent
- Configuration reference,
budgets[]