Usage Limits & Budget Rules
Set spending, token, and request budgets for your apps, pools, and gateway to control how much they can use.
Budget Hierarchy
Limits at every level apply together. The gateway checks its own limits, parent pool limits, and app limits. An exhausted Block limit rejects the request unless the normal provider path uses a configured quota fallback. Warn limits allow the request and report the warning. Budgets cover these levels:
- Gateway - Limits across the Zuplo project (for example, $1,000/day), covering all pools, sub-pools, and apps combined
- Pools - Limits covering the pool's own usage and every descendant sub-pool and app (for example, $500/day for the Engineering pool)
- Apps - Per-app limits for granular control (for example, $10/day for a hackathon app)
The User App isn't part of a pool. It has a per-person budget, set on the Users tab, and gateway limits apply to it as well.
For example, a pool has a $50 monthly shared Block budget. App A spends $40
and App B spends $10. Once that usage is reflected in the budget check, both
apps have reached the pool's budget. Without a quota fallback, requests through
both apps receive 429 Too Many Requests. An app outside this pool doesn't
share its $50 budget, but still has its own app, pool, and gateway limits.
App budgets can add up to more than their pool's budget. The pool's budget caps the combined usage; increasing an app's budget doesn't increase its pool's cap. The same rule applies to nested pools and the gateway root.
Where limits are configured
| Level | Budgets | Where to configure |
|---|---|---|
| Gateway | Shared | Settings → Usage Limits |
| Pool | Shared | The pool's Usage & Limits tab |
| App | Shared and per-metadata | Budgets and Costs in the app's policy chain |
Use Budgets and Costs to set an app's own budget.
New apps inherit their initial budget settings from their pool policy template. To limit the pool's combined usage, set a budget on Usage & Limits.
How a budget rule works
Each budget rule defines who shares the budget and how much they can use.
Scope decides who shares the budget:
| Scope | What it budgets |
|---|---|
| Shared | One budget for the app, pool, or gateway |
| By metadata | A separate budget for every distinct value of an expression |
A By metadata rule gives each distinct expression value its own allowance.
With the expression request.headers.get("x-user") and a $5 daily cap, every
distinct x-user value gets its own $5 per day—one user exhausting their budget
doesn't affect anyone else. An app can combine a shared budget with several
per-metadata budgets.
Limit rows set the caps. Each row is a meter, a period, an amount, and an action:
| Field | Values |
|---|---|
| Meter | Cost (dollars), Tokens (input plus output), Requests |
| Period | Hourly, Daily, Weekly, Monthly |
| Limit | An absolute amount in the meter's own unit |
| Action | Block rejects the request; Warn notifies without blocking |
Add as many rows as you need to cap several meters and periods at once. A Warn row and a Block row on the same meter and period give you warn-then-block—warn at $80, block at $100. The warn amount has to be the lower of the two.
The Overview tab charts daily and monthly usage only. Hourly and weekly rows are enforced, but no meter on that tab tracks them.
Budget rule periods follow the UTC calendar
Each limit counts all applicable usage since the start of its current period: the top of the hour for hourly limits, 00:00 UTC for daily, Monday at 00:00 UTC for weekly, and 00:00 UTC on the first of the month for monthly. Usage resets at the next boundary. Custom anchors aren't configurable.
A limit added mid-period counts usage from the start of that period, not from when you add it. For example, an app that has spent $150 since the first of the month and gets a $100 monthly Block limit on the 20th has already exhausted that limit, so its requests follow what happens when a limit is exceeded until the next month begins. Changing a limit's amount, or removing a limit and adding it again, doesn't reset the period's usage.
Setting an app's limits
-
Open the Apps tab and select the app.
-
On the Policies tab, configure the Budgets and Costs policy (add it from Add policy if the chain doesn't have it).
-
Select Add rule, choose the scope, and use Add limit to add rows for the meters and periods you want to cap. For a By metadata rule, enter the expression that identifies each caller.
-
Select Apply, then Save changes on the policy chain. See Propagation and accounting delays for when the change takes effect.
The app editor shows pool and gateway budgets under Inherited. To change one, open the pool or gateway where you set it.
Budget expressions
A By metadata rule identifies each caller with an expression evaluated per request. The editor validates it as you type. Available values include:
request.headers.get("x-user")and other request headersrequest.user.subandrequest.user.data.<key>from the authenticated API keyrequest.url,request.method,request.query,request.searchParams, andrequest.paramscontext.contextId,context.requestId,context.route, andcontext.custom
The editor labels each expression by where its value comes from, because that
decides how much the budget can be trusted. A client-supplied value—a header
or query parameter—is attribution, not enforcement: a caller who changes it gets
a fresh budget. Budget on request.user.*, labeled verified, when the
segment itself has to hold, and keep a shared-budget rule alongside it as the
hard cap.
Changing an expression starts a new budget. Usage recorded under the old
expression doesn't carry over, and the new expression can't count requests it
never evaluated, so each value's budget usually starts from zero. If you switch
back to an expression used earlier in the current period, the usage recorded for
it during that period still counts. Expressions are also case-sensitive:
get("X-User") and get("x-user") read the same header but budget separately.
To configure budgets in JSON, use the budgetRules option in Budgets and
Costs. For the available options, see the
Budgets and Costs policy reference.
This rule set budgets each user, each organization, and the app as a whole:
Code
Every distinct x-user value gets its own 20,000 tokens per hour, resetting at
the top of the hour. Every distinct x-organization value gets its own $50 per
day, resetting at 00:00 UTC. The app rule caps the whole app at $2,500 per
month, warning at $2,000, resetting on the first of the month.
Checking what a rule resolved to
A By metadata rule whose expression resolves to nothing budgets nothing, and the request still succeeds. A rule that silently does nothing therefore looks exactly like a rule that's working. Your gateway's request logs say which it is.
Every request that runs at least one By metadata rule logs
AI Gateway budget dimensions resolved, listing the rules that resolved and the
rules that didn't. Each rule that didn't carries a reason:
| Reason | What to change |
|---|---|
no-such-property | The expression is valid but found no value. The message names the property that broke the chain—check that the header or claim is really on the request. |
invalid-syntax | The expression is outside the grammar. The message gives the position of the problem. |
per-request-identifier | The value is unique to every request, so no two requests share a budget and no threshold is ever reached. Key on something that repeats, like a user or an organization. |
internal-field | The value comes from a field the proxy populates, such as client certificate material. |
value-not-scalar / value-not-safe-integer | The value isn't a string or a whole number. |
value-too-long | The value is longer than 256 bytes. |
value-has-control-characters | The value contains control characters. |
Resolved entries record the length of each value, not the value itself. A rule
whose expression names a credential—anything whose text contains token,
secret, authorization, password, cookie, credential, bearer, or
api_key in any spelling—never logs its value at all. Budgeting per API key is
a reasonable thing to do, and the key stays out of the log.
A rule that can never work, because its expression is unusable or the rule fails validation, also logs a warning. Those warnings arrive once per configuration rather than once per request, so a broken rule doesn't flood the log.
Verdicts name their rule too. A block or warning from a By metadata rule
carries the expression and its ruleId; one from an app-wide budget carries
neither. That's how you tell which rule refused a request.
Setting pool and gateway limits
A pool budget covers the combined usage of its apps and sub-pools. A gateway budget covers all apps in the project.
To add a shared pool rule, select the pool or sub-pool in Apps, then open Usage & Limits. To add a shared gateway rule, open Settings → Usage Limits. Both editors use the same meter, period, amount, and action rows as the app editor.
The editor shows budgets from parent pools and the gateway under Inherited. To change an inherited budget, edit it on the pool or gateway where you created it.
When a limit is exceeded
For a request continuing to a provider, an exhausted Block limit uses the app's configured quota fallback model instead of blocking. See Fallback Models, where the fallback model is selected. This applies to gateway and pool limits as well as the app's own. The fallback's usage still counts toward the limits.
Without a quota fallback on that path, the request is rejected with
429 Too Many Requests and a budget object naming what ran out:
Code
The scope tells the caller which budget they hit. It is application when the
gateway, a pool, or the app exhausts a shared budget. It is dimension when a
value exhausts an expression budget. Quote the ruleId when raising a support
request.
Successful cache responses
Cached responses are subject to budget limits too. If a Block budget is
exhausted, a cache hit returns 429 Too Many Requests instead of the cached
answer. Quota fallback doesn't apply to cache hits. Warn limits allow the
cached answer through.
Authentication failures and other error responses keep their original response; the budget check doesn't replace an authentication error with a budget error.
Budgets fail open by default
If a pool or gateway budget check fails, the request can continue. However, a
failed check doesn't override a budget violation already detected: a cache hit
still returns 429 if the app's budget check has found an exhausted Block
limit.
To reject requests when Budgets and Costs can't complete its own checks,
enable Fail closed when metering is unavailable (throwOnFailure). This
option is off by default and doesn't change how pool or gateway check failures
are handled.
Propagation and accounting delays
Budget changes don't take effect immediately. After editing a budget or moving an app to another pool, allow time for the saved settings and usage totals to update. The delay depends on your gateway's cache settings.
Requests already in progress and delays in updating usage totals can take spending past a budget before further requests are blocked.
Monitoring Usage
Each app, pool, and the gateway show current usage against their limits:
- Open the Apps tab and select an app or pool
- The Overview tab shows daily and monthly usage—spend, tokens, and requests—with progress against any configured limits
- The same tab's Metrics frame charts request count, token usage, and cost over time; View in Analytics opens the full request-level breakdown scoped to that app or pool
Related Resources
- Getting Started - Set up your first AI Gateway project with budget controls
- Managing Pools - Configure pool-level budgets
- Managing Apps - Configure app-level limits
- Fallback Models - Serve a cheaper model instead of blocking when a limit is exceeded