Budgets & spend limits
Budgets cap what can be spent, at two levels: one workspace budget sets the ceiling, and each member can hold an allocation out of it. The member allocations may never sum past the workspace ceiling — that invariant is what makes the two levels a budget rather than two independent numbers. A third, separate control — end-user spend limits — caps what your app's individual end users can spend; see the end of this page.
#The two levels
| Level | How many | Meaning |
|---|---|---|
| Workspace | Exactly one per workspace | The ceiling for everything in the workspace. |
| Member | At most one per member | That person's slice of the ceiling. |
Both are rows in the same table, distinguished by whether they name a user. Set them in the panel under Budgets → Applies to: The whole workspace or A single member.
The owning workspace is never typed in — it comes from the workspace you have selected (the ?w= in the URL), and the server stamps it on write. A budget filed against a workspace you aren't in is not expressible.
#Rules
Three rules are enforced on every write, and the error tells you which one you hit:
- One workspace budget per workspace. A second is rejected — edit the existing one.
- One budget per member. Same.
- Member allocations never exceed the workspace budget. This applies in both directions: adding or raising a member budget past the free headroom is rejected, and lowering the workspace budget below what is already allocated is rejected too. The second is the one people trip over — the fix is to lower the member budgets first.
Two details worth knowing:
- Set the workspace budget first. Member budgets are allocated out of it, so there is nothing to allocate from until it exists.
- Disabled member budgets still count toward the allocated total. If disabling one freed up headroom, re-enabling it could silently push the workspace over its ceiling. Delete it to reclaim the allocation.
The one-per-workspace and one-per-member rules are also filtered unique indexes in the database, so they hold under concurrent writes, not just in the UI.
#Periods and modes
Each budget has a period (daily / weekly / monthly — weeks start Sunday 00:00 UTC, months on the 1st) and a mode:
- Hard stop — once spend reaches the limit, the gateway rejects further requests with
402 budget_exceeded. - Alert only — the limit is tracked and shown, but nothing is blocked.
Spend is not counted per request in SQL. BudgetRefreshService recomputes it from telemetry every Gateway:BudgetRefreshMinutes (default 5), so a hard stop engages within that window rather than instantly — size the cap with that lag in mind, and note that a budget with telemetry unconfigured never advances at all.
#What is enforced at request time
Workspace budgets are enforced. ModelRouter checks them before dispatch, so the 402 costs nothing upstream. The check covers workspace-level and global budgets. A batch job checks the budget once, when its model is resolved at job start, not per row.
Member budgets are not enforced at request time — they are an allocation and governance control. This is a real limitation and worth being explicit about: the gateway cannot attribute a request to a panel member. An API key carries a workspace claim and nothing else, and the end-user id a request may carry names the calling app's user (an Entra object id), not a panel account — the two are different id spaces. So a member's spend has nowhere to come from.
Two consequences follow, and the panel shows both rather than hiding them:
- A member budget's spend reads as "not tracked", not as
$0.00— those are different claims, and a burn bar pinned at 0% would assert the wrong one. - Member rows are excluded from the burn cards, and carry an
allocationbadge instead of a hard-stop / alert-only mode.
What member budgets do give you today is a governed division of the ceiling: an agreed, enforced-on-write split that no one can quietly exceed on paper, and a single place to see how much of the workspace budget is committed versus free. Closing the gap needs a member dimension on the API key and on the usage events — a schema change, not a wiring change.
#End-user spend limits
The spend that can be attributed per person is your app's end user, named in X-Relay-User-Id (or the OpenAI user field). On a user's page under End users, set a limit per day, week or month, hard stop or alert only. With a hard stop, that user's requests through the chat pipeline are refused with 402 end_user_budget_exceeded once their spend reaches it. Spend is refreshed with the workspace budgets (Gateway:BudgetRefreshMinutes) and the limits themselves are cached per workspace for Gateway:EndUserBudgetCacheSeconds (default 30). These are keyed by your app's end-user id and are entirely separate from panel member budgets. See Observability.
#Reading the numbers
The Budgets page shows, for the workspace budget: spend against limit, percentage used, and a projection ("on track — ~72% projected by Mar 31", or the date the limit is expected to be hit). Above the table, an allocation summary reports how much of the ceiling is committed to members and how much is still free.
#Related
- Authentication & keys — keys are how spend is attributed to a workspace.
- Telemetry & dashboards — where spend figures come from.
- Cost API — per-model rates and cost estimation before you commit.