docs / reference
Usage Quotas
Give each plan an allowance like "Free gets 20 generations a day", show users what they have left, and know when to suggest an upgrade.
A usage quota limits one end user by request count or by USD spent, over a day, week, or month. It is checked on every check() call, before the LLM call fires, the same way budget caps are.
Most AI apps price by plan: Free gets 20 generations a day, Pro gets 500. Quotas let you say that directly instead of converting it into a dollar cap.
How a quota is chosen
For each check() that has an end_user_id, noburn picks one quota:
- A per-user cap, if you set one for that user under Per-User Budget Caps. It overrides the plan.
- Otherwise, the plan quota whose plan key matches the
planyou pass incontext. - Otherwise, no quota.
quotaisnullin the result.
The plan key is any string you choose. noburn does not interpret it: pass whatever your billing system calls the plan (free, pro, team_annual).
What counts
| Limit type | Counts | A call is blocked when |
|---|---|---|
requests | Non-blocked calls you record() for that user in the window | used + 1 > limit |
usd | Dollars spent by that user in the window | used + estimated cost > limit |
Blocked calls never count against the allowance.
Windows are UTC
| Window | Starts | Resets |
|---|---|---|
day | 00:00 UTC | the next 00:00 UTC |
week | Monday 00:00 UTC | the next Monday 00:00 UTC |
month | the 1st, 00:00 UTC | the 1st of the next month |
Every user resets at the same instant regardless of their timezone. resetsAt (resets_at in Python) gives you the exact time, so you can show it in the user's local time.
Set up a plan quota
Open your project, find Plan Quotas, and add a row: plan key free, type Requests, value 20, window per day. Edit or remove it at any time. Changes apply on the next check().
Pass the plan on every check
decision = guard.check(
model="gpt-4o",
estimated_tokens_in=1000,
estimated_tokens_out=300,
end_user_id=user.id,
context={"plan": user.plan}, # "free"
)
if decision.blocked:
q = decision.quota
if decision.block_reason == "quota_exceeded" and q and q.upgrade_suggested:
return show_upgrade(f"You've used all {q.limit:g} generations today.")
return show_error(decision.block_reason)const decision = await guard.check({
model: 'gpt-4o',
estimatedTokensIn: 1000,
estimatedTokensOut: 300,
endUserId: user.id,
context: { plan: user.plan }, // 'free'
});
if (decision.blocked) {
if (decision.blockReason === 'quota_exceeded' && decision.quota?.upgradeSuggested) {
return showUpgrade(`You've used all ${decision.quota.limit} generations today.`);
}
return showError(decision.blockReason);
}Show "12 of 20 left"
remaining() reads the allowance without making a call, so you can show it before the user tries anything.
q = guard.remaining(end_user_id=user.id, plan=user.plan)
if q:
label = f"{q.remaining:g} of {q.limit:g} left" # "12 of 20 left"
if q.upgrade_suggested:
show_upgrade_banner()const q = await guard.remaining({ endUserId: user.id, plan: user.plan });
if (q) {
label.textContent = `${q.remaining} of ${q.limit} left`; // "12 of 20 left"
if (q.upgradeSuggested) showUpgradeBanner();
resetsAt.textContent = new Date(q.resetsAt).toLocaleTimeString();
}remaining() returns null (None in Python) when no quota applies to the user, when noburn cannot be reached, or when the lookup fails on the server (503 QUOTA_UNAVAILABLE). It never throws, except NoburnAuthError for an invalid key. If it returns null, hide the counter rather than showing zero.
When to suggest an upgrade
upgradeSuggested is true only when a plan quota is what stops the user:
- In
check(), when the call was blocked withquota_exceededby a plan quota. - In
remaining(), when a plan quota would block the next call. Both use the same rule. For a USD quota, pass the cost of the call you are about to make (estimatedCostUsd/estimated_cost_usd) to get exactly the answercheck()would give. Without it, a USD quota counts as used up once no billable call fits.
It stays false when a per-user cap blocks the user. That cap is a limit you set for that one person, and changing their plan would not lift it. It is also false when something else blocks the call first, such as the project budget or a policy rule.
The quota object
| Field (Node / Python) | Type | Description |
|---|---|---|
limitType / limit_type | 'usd' | 'requests' | What the quota counts |
limit | number | The allowance |
used | number | Usage in the current window |
remaining | number | max(0, limit - used) |
window | 'day' | 'week' | 'month' | The UTC window |
resetsAt / resets_at | string | ISO time the window resets |
plan | string | null | The plan you passed |
upgradeSuggested / upgrade_suggested | boolean | See above |
quota is null when no quota applies, and absent when check() fell back to its local decision because noburn was unreachable.
Per-user caps
A per-user cap now also has a type and a window, so you can give one customer 100 requests a week. A cap of USD per month behaves exactly as per-user caps always have: it blocks with user_budget_exceeded and fills userCapUsd / user_cap_usd. Any other type or window blocks with quota_exceeded.
If noburn can't read a quota
check(): if the plan-quota lookup or the usage count fails on the server, that call skips the quota. Every other limit still applies: the project cap, the run cap, policy rules, and a USD-per-month per-user cap. A per-user cap of any other type or window, and every plan quota, is skipped for that call.remaining(): returnsnull(None) when the server answers503 QUOTA_UNAVAILABLE.
end_user_id and plan are limited to 255 characters. In check(), a longer end_user_id is ignored, so no per-user cap or quota applies, and a longer plan is treated as no plan. The call is still checked against the project cap, the run cap and policy rules, and the server logs a warning. GET /api/v1/quota answers the same input with a 400, so remaining() returns null.
Quotas or policy rules?
Use a quota when the answer depends on how much a user has already used. Use a policy rule for a decision about the call itself, such as "never let free users call gpt-4o".