Creating a budget
AI Gateway > Budgets > New budget, or over the API:timezone, and your own external_id and metadata.

The budget drawer targets any dimension, with the provider filter as an orthogonal choice.
Scopes
demo of team platform in org acme with VK prod-key is checked against that org’s, team’s, project’s and VK’s budgets, Alice’s principal budgets, and the per-member budgets of every group Alice belongs to. If any budget hard-blocks, the request is rejected; if any warn threshold is crossed, a warning header is added. Hard-block wins over warn.
A budget has to reach the traffic it names
Which team and project a request belongs to is decided by the key that served it: the teams the key is scoped to, plus the team of the project its traces land in. So ateam, project or group budget can be written, look correct on both sides, and match nothing at all, for instance a project budget on a project none of your keys send traffic to.
A budget that could never accrue is refused when you create it, with gateway_budget_scope_unreachable. The refusal lists the projects your active keys do reach, in error.meta.reachable_project_ids. Two ways forward:
- Point a key at the target: give it a scope on that team or project, or set the key’s trace project.
- Keep the budget as written by sending
allow_unreachable: true, which is the right answer when you are provisioning ahead of the keys that will use it. In the app, the refusal offers “Create it anyway”.
scope_reach on each budget says reachable or unreachable, which is the only field that tells a budget nothing can reach apart from one that has not been breached.
Group budgets are per member
Agroup budget targets one of your Groups (the same entity SCIM provisions) and gives each member their own allowance, not one shared pot: a 10 and Bob at $10, and Bob’s spend never counts against Alice. One budget row covers every member, and someone joining the group is covered on their key’s next configuration resolve.
Two consequences worth knowing:
- Group budgets only apply to keys with a person behind them (a personal VK, or any VK with a principal). A shared service key has no member to charge the allowance to, so cap it with
project,team, orvirtual_keybudgets instead. - Group budgets require the ClickHouse spend ledger, because per-member enforcement needs each member’s spend told apart. Deployments reading budget spend from Postgres only refuse the create with
group_budget_requires_clickhouserather than silently capping every member at the group’s combined spend.
Per-end-user budgets (attributed-user templates)
Anattributed_user budget is a template, not a cap on one person: anchored on a virtual key or a project, it means “each distinct end user seen here: this limit per window”. One row covers every current and future end user; per-user buckets appear lazily on first spend, keyed <anchor_id>:<end_user_id>, and there is nothing to provision or delete per user. This is the cap layer for platforms that rebill their own customers, whose end users are not LangWatch users (for real LangWatch accounts, use principal or group).
- The end-user id is resolved from the request: the
x-langwatch-end-user-idheader, then thex-litellm-end-user-idalias, then the OpenAIuserbody field. Headers win over the body. The same resolution feeds spend events, so metering and enforcement always agree about who a request was. - Fail closed: while a template is active on the anchor, a request carrying no end-user id anywhere is rejected with
error.code = "end_user_required", naming both accepted wire fields. A cap evadable by omitting a field is not a cap. The rejection is still recorded per key, and it produces no provider spend. - Enforcement is per bucket: one end user at their limit blocks only that user; every other user on the anchor passes. Breaches name
budget_scope: "attributed_user"so your product can tell “this user’s cap” from the tenant cap. - Like group budgets, templates require the ClickHouse spend ledger.
- Read one user’s current spend and applicable cap in one call:
GET /api/gateway/v1/end-users/:id/spend. - How a template reports itself. One template covers many people, so there is no single total to measure anyone against. The Budgets list and detail page headline the per-person cap (“$1.00 per person”) and put the standing underneath as a headcount: “3 of 10 people over cap”, the bar filled to the same ratio and red as soon as anyone is over. “Seen” means an end user with at least one successful request in the current period, so somebody served entirely by an unpriced model still counts, while somebody whose every request failed does not. Over-cap uses the same
>=the gateway blocks on, so a person the page calls over is a person being refused. The same two figures ride the API asend_users_seenandend_users_over.
Provider-filtered budgets
Any budget, on any scope, can additionally be filtered to a single provider: “OpenAI $200/month for this team” is one budget with a provider filter, not a new kind of scope. With a filter set:- Only requests dispatched to that provider debit the budget. Every ledger debit stamps the provider that actually served the request, so two budgets on the same target with different filters never share spend.
- On breach, the filtered provider is removed from the request’s candidate chain the way an unavailable provider would be. If the key can fall back, another provider serves the request and the caller sees nothing but a slower first token. Only when the exclusions leave the chain empty is the request blocked, with a
402naming the budget inerror.meta.budget_idand the provider inerror.meta.budget_provider.
Windows
minute, hour, day, week, month, total, manual.
- The five cycling windows roll on a schedule, computed in UTC. By default they align to the calendar:
dayresets at 00:00,weekon Monday 00:00 (ISO 8601),monthon day 1 at 00:00. Thetimezonefield exists on the API for forward compatibility, but enforcement does not honor it yet, so the UI states UTC and offers no timezone choice. totalnever resets, which suits one-off grant-style budgets.manualaccrues until you reset it: the period boundary moves only when your system says so. Without it a reset endpoint alone would not help you, because amonthbudget would still calendar-reset mid-cycle.
Anchoring a cycle to your own date
Setcycle_anchor_at on a cycling window and the period rolls from that instant instead of the calendar. This is how you line a budget up with a billing date: anchored on the 17th at 09:00, every period starts on the 17th at 09:00.
- The anchor day is never rewritten. A monthly cycle anchored past the 28th clamps into shorter months and springs back: anchored on Jan 31, the periods start Feb 28 (Feb 29 in a leap year), then Mar 31, then Apr 30, then May 31.
- Fixed windows are pure modulo from the anchor. A week anchored on a Saturday rolls Saturdays; the ISO Monday plays no part.
- A future anchor is allowed. The budget reads zero and reports the anchor as its reset until the anchor arrives, which is how you say “start cycling on the 1st of next month”.
totalandmanualrefuse an anchor, with400 gateway_budget_cycle_anchor_invalid. Those windows do not cycle, so an anchor would be a silently ignored instruction.- The anchor is fixed at creation. Changing the clock mid-period is a reset in disguise, and reset already exists. Update requests carrying
cycle_anchor_atleave it untouched.
current_period_started_at and resets_at are computed when you read the budget, for every budget, so the pair always describes the period you are actually in.
Resetting a period
POST /api/gateway/v1/budgets/:id/reset moves the boundary to now and recomputes the next reset:
- Recorded spend is never mutated. The ledger and every emitted billing event are immutable, so reconciliation is unaffected. What changes is the lower bound of “spend inside the current period”.
- On a cycling window, reset truncates the running period and the next boundary stays on its schedule, calendar or anchored. On
manualthe new period stays open until the next reset. - On attributed-user templates,
?end_user_id=resets one end user’s bucket and leaves the template period untouched. - Resets take the same permission as edits (
gatewayBudgets:update) and are audit-logged with the actor and an optionalreason.
on_breach
block, hard-cap. The next request afterspent_usd >= limit_usdreturns402 budget_exceededwith an OpenAI-compatible error envelope.warn, soft-cap. The request passes but the response gainsX-LangWatch-Budget-Warning: <scope>:<pct_used>. Multiple soft caps can fire on one request.
Breaches, warnings, and budget events
Three signals fire around a limit, each with a distinct consumer:- The
402is for the caller whose request was rejected. Itserror.metais machine-readable:budget_id(which allowance to raise),budget_scope(lowercase),budget_window, andbudget_providerwhen a provider filter emptied the chain.budget_scopeis the branch a rebilling platform’s copy needs:attributed_usermeans the end user’s own allowance,virtual_keymeans the tenant cap. - The warning header is for the caller whose request still passed: any budget at or past 80 percent contributes
X-LangWatch-Budget-Warning: <scope>:<pct_used>, whatever itson_breach. Awarnbudget past its limit stays a warning; ablockbudget warns on approach before it starts rejecting. - The webhook events are for your backend, so it learns without polling: crossing 80 percent emits
gateway.budget.threshold_crossed, reaching the limit emitsgateway.budget.breached(onwarnbudgets too). Each fires once per crossing per period: re-crossing inside the same period is deduplicated, and a new period starts the cycle fresh. See Webhooks.
end_user_required, the fail-closed refusal when an attributed-user template is active and the request carried no end-user id. It is a request-shape problem, not a breach: the fix is sending the id, not raising a limit.
Multiple tiers on the same scope
A single scope can carry several budgets with differenton_breach values. Common pattern:
- Budget A:
team,month,$4000,warn. SurfacesX-LangWatch-Budget-Warning: team:80once 80% of the real ceiling is spent. - Budget B:
team,month,$5000,block. Hard-caps at $5k.
Viewing budget spend

The budgets list: one row per budget across every dimension, each with its utilization bar and reset.

A breached budget's detail page: full utilization, the enforcement mode, and the debits that consumed it.

Usage on the This month preset, filtered to one key: the same window and the same figures as the spend column that linked here.
How spend is counted
The debit model, the pre-request check, and what they trade off
The debit model, the pre-request check, and what they trade off
gateway_request_id and is idempotent by that id, so a retry or a redelivered batch never double-bills.- The gateway admits the request before any budget or guardrail gating runs, recording who it belongs to. Admission is a local, non-blocking write to a bounded on-disk spool, so the request path never waits on the control plane.
- The gateway streams the response. When it closes, it records the provider’s token counts as a confirmation, or the failure with its error class. Both go to the same spool.
- A background drainer ships spooled batches to the control plane, which prices each outcome once from tokens times the pricing catalog.
- The debits process resolves every applicable budget and writes one ledger row per budget, rolled up by a materialised view. It is the only writer of that ledger.
- The same debit emits a change event, so the gateway drops its cached bundle for that project and the next request enforces against fresh spend.
PROVIDER_ERROR, or BLOCKED_BY_GUARDRAIL) and show in a budget’s activity list, but they do not count toward a cap: only successful requests accrue enforcement spend. A platform failure should be visible to you without charging your customer for it.The pre-request check. Before dispatching, the gateway evaluates the key’s in-memory budget snapshot, which arrives baked into its cached bundle. No control-plane round trip on the hot path: a block scope with nothing remaining is rejected outright, saving the provider spend and the round trip, and anything at or over 80% is allowed and flagged as a warning.The exception is an attributed-user template, where one row governs a separate allowance for every end user the anchor will ever see. That fan-out cannot be baked into a bundle, so the gateway reads the request’s own bucket from the control plane, cached 15 seconds per (budget, end user). A bucket it cannot read skips that scope: unreadable allows, it never blocks.The stale-snapshot trade-off. Because the check is local and the snapshot lags the ledger by one refresh, two replicas can each see spent=$24.90 / limit=$25.00 and both admit a 25.80 against a $25 cap. This is deliberate: a control-plane call on every near-limit request would put the gateway’s availability at the mercy of the control plane, and the overshoot is bounded by one refresh interval of traffic. Precheck is permissive by design, so a control-plane outage degrades enforcement accuracy, never availability, and spend reconciles once the ledger catches up.What “USD cost” means. Tokens times unit price, per provider, from their published pricing. Cache reads and writes are priced separately per provider: Anthropic cache reads are about 10% of regular input tokens and cache writes 125%. See Caching passthrough.spentUsd column is the fallback for self-hosted installs without it. A 1 of spend on either.Permissions
Trace attributes
langwatch.cost_usd, cost of this single request.langwatch.budget.breached_scope, present when the request was blocked; formatscope:window(for exampleproject:month).langwatch.budget.warnings, comma-separatedscope:pct_usedentries for soft breaches.