Skip to content

Accounting

FastAPIAccounting, its quotas, usage reports and records, and their sinks. In fastapi_alcazaba.accounting.

In the guide: Accounting. In an app: the example.

fastapi_alcazaba.accounting.FastAPIAccounting

FastAPIAccounting(*, subject: Subject, quotas: Iterable[Quota] = (), period: Period = 'month', sinks: Iterable[UsageSink] | None = None, cache: Cache | None = None, fail_open: bool = True, challenge: str = 'Bearer', headers: bool = True, timeout: float = 5.0, metrics: Metrics | None = None)

Meters what each subject uses, per period, and holds it to its quotas.

meter(name) is a dependency that charges each request: 1 unit by default, checked against the quota and counted in one atomic operation, so concurrent requests never go past it. A request over its quota gets a 429 with Retry-After (when the period ends), or a 402, and isn't counted. meter(name, amount=None) gives the endpoint a Metering to add what it uses as it goes (tokens, bytes, seconds), charged when it ends; the quota is checked when it starts, so the requests already running may together go past it.

Responses carry the RateLimit and RateLimit-Policy headers of the IETF draft, the quota's name being the meter's, for clients to see what they have left.

What's charged: a request that gets a client error (a 4xx, a validation error) was served and stays charged; one that fails on the server (an exception, a 5xx) or whose client went away is refunded, and has no record. What an endpoint added to its Metering is charged whatever happens next: it was used.

Parameters:

Name Type Description Default
subject Subject

a dependency that returns who uses it: a user's, an API key's or a tenant's ID, None when nobody is authenticated (a 401, WWW-Authenticate: <challenge>).

required
quotas Iterable[Quota]

a Quota per meter at most. Meters without one are counted, not bounded.

()
period Period

the period of the meters without a quota.

'month'
sinks Iterable[UsageSink] | None

where the usage records go; a LoggingUsageSink by default, () for none.

None
cache Cache | None

the Cache whose backend keeps the counters; the app's (get_cache) by default.

None
fail_open bool

when the backend fails, let requests through uncounted (the default: the records still have them) or refuse them with a 503.

True
challenge str

the WWW-Authenticate of the 401s.

'Bearer'
headers bool

send the RateLimit and RateLimit-Policy headers.

True
timeout float

seconds each sink has to write a record.

5.0
metrics Metrics | None

where to count the requests (fastapi_alcazaba.accounting), the units (fastapi_alcazaba.accounting.usage) and the records not written (fastapi_alcazaba.accounting.undelivered); the cache's by default.

None

meter

meter(meter: str, *, amount: int | None = 1) -> Callable[..., AsyncIterator[Any]]

A dependency that charges amount units of meter to the request's subject before the endpoint runs, within its quota, and returns the subject's ID. With amount=None it returns a Metering, to which the endpoint adds what it uses; the total is charged when it ends.

record async

record(subject: str | int | UUID, meter: str, amount: int, *, request: HTTPConnection | None = None) -> UsageRecord

Charges amount units of meter to subject without checking its quota (it's been used already): for what happens outside a metered request, such as a job or a webhook. Counted in the backend (when it answers) and sent to the sinks; returns the record.

usage async

usage(subject: str | int | UUID, meter: str, *, at: datetime | None = None, request: HTTPConnection | None = None) -> UsageReport

What subject has used of meter in the period that holds at (now by default), and what its quota leaves. The counters keep the current period and the one before; the records have the older ones. Raises BackendUnavailable when the backend fails.

fastapi_alcazaba.accounting.Quota dataclass

Quota(meter: str, limit: Limit, period: Period = 'month', status: int = HTTP_429_TOO_MANY_REQUESTS)

How much of meter a subject may use per period.

Quota("requests", limit=10_000)  # a month, the same for everybody
Quota("tokens", limit=tokens_of_plan, period="day")  # each subject's plan says

Parameters:

Name Type Description Default
meter str

what it bounds, as FastAPIAccounting.meter() names it.

required
limit Limit

the units allowed per period, 0 for none; or limit(subject), a function (or a coroutine function) of the subject's ID that gives them, None for no limit.

required
period Period

a calendar hour, day or month, in UTC: the counter starts again at 0 when it ends.

'month'
status int

what a request over the quota gets: 429 (Too Many Requests, with Retry-After: when the period ends) or 402 (Payment Required: a plan to upgrade).

HTTP_429_TOO_MANY_REQUESTS

fastapi_alcazaba.accounting.Metering

Metering(meter: str, subject: str, remaining: int | None)

What meter(name, amount=None) gives the endpoint: it adds what it uses as it goes (add(tokens)), and the total is charged when it ends.

add

add(amount: int) -> None

Add amount units to what the request has used.

fastapi_alcazaba.accounting.UsageReport dataclass

UsageReport(subject: str, meter: str, period: str, used: int, limit: int | None, resets: datetime)

What a subject has used of a meter in a period, and what its quota leaves.

Parameters:

Name Type Description Default
subject str

the subject's ID.

required
meter str

the meter.

required
period str

the period's label: 2026-11, 2026-11-01, 2026-11-01T10 (UTC).

required
used int

the units counted.

required
limit int | None

the quota's limit; None without a quota, or when its limit gives None.

required
resets datetime

when the period ends, and the counter starts again.

required

remaining property

remaining: int | None

What's left of the quota; None without one.

fastapi_alcazaba.accounting.UsageRecord dataclass

UsageRecord(id: str, time: datetime, subject: str, meter: str, amount: int, period: str, request_id: str | None = None, trace_id: str | None = None, method: str | None = None, route: str | None = None)

A charge, as the sinks get it; to_dict() and to_json() give its JSON form.

Parameters:

Name Type Description Default
id str

random, unique: billing counts each record once, should a sink deliver it twice.

required
time datetime

when it was charged (UTC).

required
subject str

who used it: a user's, an API key's or a tenant's ID.

required
meter str

what was used, such as "requests" or "tokens".

required
amount int

how much, in the meter's units.

required
period str

the period it was charged to, as its counter keeps it: 2026-11 (a month), 2026-11-01 (a day) or 2026-11-01T10 (an hour), in UTC.

required
request_id str | None

the request's ID (RequestIDMiddleware).

None
trace_id str | None

the OpenTelemetry trace it belongs to.

None
method str | None

the request's HTTP method.

None
route str | None

the template of the route that matched, e.g. "/answers/{model}".

None

to_json

to_json() -> str

One line of JSON, ASCII only: nothing in it can split a log line.

fastapi_alcazaba.accounting.UsageSink

Bases: Protocol

Where usage records go. write gets each one when it's charged, within the FastAPIAccounting's timeout: queue it if writing takes longer. What it raises is logged, with the record.

fastapi_alcazaba.accounting.LoggingUsageSink

LoggingUsageSink(log: str | Logger = 'fastapi_alcazaba.accounting.usage', level: int = logging.INFO)

Writes each record as one line of JSON to the logger log (a name or a Logger), at level, for the log pipeline to keep and add up; handlers that format their own JSON find the record in the log record's usage attribute too. When the logger outputs nothing at that level (no handler, or a higher level), it says so once, as a warning.

fastapi_alcazaba.accounting.MemoryUsageSink

MemoryUsageSink(maxlen: int | None = 1000)

Keeps the last maxlen records in records: for tests, and to look at them while developing.

fastapi_alcazaba.accounting.period_of

period_of(period: Period, at: datetime) -> tuple[str, datetime, datetime]

The period that holds at: its label (2026-11, 2026-11-01, 2026-11-01T10), when it starts and when it ends, in UTC.

fastapi_alcazaba.accounting.Period module-attribute

Period = Literal['hour', 'day', 'month']