---
title: "Per-user AI budgets"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:administration-user-budgets@0.35"
source: "Administration/UserBudgets.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# Per-user AI budgets

The `tx_nrllm_user_budget` table caps per-backend-user AI spend
independently of the per-configuration daily limits on
`tx_nrllm_configuration`. A user request must clear BOTH layers:
any limit on the preset they chose AND any limit on their personal
budget record.

## What a budget caps

Each row in `tx_nrllm_user_budget` binds to exactly one
`be_user` and defines six independent ceilings. `0` on any axis
means "unlimited on this axis".

| Field | Unit | Reset cadence |
| --- | --- | --- |
| Max Requests/Day | count | Every day at 00:00 server-local time. |
| Max Tokens/Day | count | Every day at 00:00 server-local time. |
| Max Cost/Day ($) | USD | Every day at 00:00 server-local time. |
| Max Requests/Month | count | First of the month, 00:00 server-local time. |
| Max Tokens/Month | count | First of the month, 00:00 server-local time. |
| Max Cost/Month ($) | USD | First of the month, 00:00 server-local time. |

Usage is aggregated on demand from `tx_nrllm_service_usage` — the
same table the UsageTracker already writes to per request — so there
is no second write per request and no way for a separate counter to
drift away from the source of truth.

## Creating a budget

Budget records have `rootLevel = -1`, so admins can create them at
the TYPO3 root (`pid = 0`) or on any regular page. Keeping them at
the root is the convention because budgets are site-wide admin
concerns, not page-scoped content; the recipe below follows that
convention.

1.  Open **Web > List** in the root (page UID 0) — or on the
    page where you keep other cross-site configuration records.
1.  Click **Create new record**.
1.  Choose **LLM User Budget**.
1.  Pick the backend user, set the ceilings, toggle
    **Enforce this budget** on.
1.  Save.

> [!NOTE]
> Only one budget row per backend user. The `be_user` column
> is unique. Re-editing the existing row is the correct way to
> tighten or relax limits.

## How the check runs

Before dispatching a request the consuming extension calls
`NetresearchNrLlmServiceBudgetService::check()`. The service:

1.  Returns *allowed* when the user has no budget record, when
    **Enforce this budget** is off, or when every ceiling
    is `0`.
1.  Aggregates today's usage and this month's usage in a single
    database roundtrip.
1.  Evaluates the daily window first; the monthly window only if the
    daily window passes.
1.  Adds `+1` request and `+plannedCost` to the usage figures
    before comparing, so a user at exactly the limit is still
    allowed one more call.

The returned `BudgetCheckResult` names which bucket was tripped
(`exceededLimit` as a stable machine key, plus a human-friendly
`reason` string suitable for log output or caller-side wrapping).

> [!IMPORTANT]
> The check is **best-effort**, not a transactionally-safe gate.
> Two concurrent requests for the same user can both pass
> `check()` before either updates
> `tx_nrllm_service_usage`, temporarily allowing a one-request
> overshoot. Full serialisation would hot-path every AI request.
> If strict enforcement matters, layer a per-user lock on top.

## Budgets vs. configuration limits

Both layers persist but cap different things:

| Axis | Configuration daily limits | Per-user budgets |
| --- | --- | --- |
| Bound to | a preset (`tx_nrllm_configuration`) | a backend user (`tx_nrllm_user_budget`) |
| Question answered | "Can ANY editor keep using this preset today?" | "Can THIS editor keep spending this month?" |
| Windows | daily | daily AND monthly |
| Dimensions | requests, tokens, cost | requests, tokens, cost |
| Both must pass | yes | yes |

See [ADR-025: Per-User AI Budgets](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-025@0.35) for the full design rationale, including the
alternatives (counter table, group-level budgets, auto-throttling)
we considered and why they were rejected.
