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:

  1. A per-user cap, if you set one for that user under Per-User Budget Caps. It overrides the plan.
  2. Otherwise, the plan quota whose plan key matches the plan you pass in context.
  3. Otherwise, no quota. quota is null in 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 typeCountsA call is blocked when
requestsNon-blocked calls you record() for that user in the windowused + 1 > limit
usdDollars spent by that user in the windowused + estimated cost > limit

Blocked calls never count against the allowance.

Windows are UTC

WindowStartsResets
day00:00 UTCthe next 00:00 UTC
weekMonday 00:00 UTCthe next Monday 00:00 UTC
monththe 1st, 00:00 UTCthe 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 with quota_exceeded by 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 answer check() 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)TypeDescription
limitType / limit_type'usd' | 'requests'What the quota counts
limitnumberThe allowance
usednumberUsage in the current window
remainingnumbermax(0, limit - used)
window'day' | 'week' | 'month'The UTC window
resetsAt / resets_atstringISO time the window resets
planstring | nullThe plan you passed
upgradeSuggested / upgrade_suggestedbooleanSee 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(): returns null (None) when the server answers 503 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".

On this page