Built-in: budgets
The budgets middleware caps row counts and makes over-latency queries visible.
The budgets middleware puts a cost ceiling on every query: it rejects or warns on queries that would read more rows than you allow, counts rows while they stream, and reports queries that run longer than your latency budget.
Register it on the runtime with the row and latency limits you want to enforce:
import postgres from '@prisma/orm-postgres/runtime';
import { budgets } from '@prisma/orm-postgres/family-runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL']!,
middleware: [
budgets({
maxRows: 10_000,
defaultTableRows: 10_000,
tableRows: { user: 10_000, post: 10_000 },
maxLatencyMs: 1_000,
}),
],
});budgets is SQL-family middleware (it declares familyId: 'sql'), so it registers on PostgreSQL runtimes. It ships with @prisma/orm-postgres/family-runtime, which comes in through @prisma/orm-postgres; you do not install anything extra.
Options
All options are optional.
| Option | Type | Default | What it controls |
|---|---|---|---|
maxRows | number | 10_000 | The most rows a single query may produce, checked against both the pre-execution estimate and the observed row stream |
defaultTableRows | number | 10_000 | Assumed row count for tables not listed in tableRows, used by the estimator |
tableRows | Record<string, number> | {} | Per-table row-count assumptions for the estimator, keyed by table name |
maxLatencyMs | number | 1_000 | Latency budget for one query, checked after execution |
severities.rowCount | 'warn' | 'error' | 'error' | Whether pre-execution row-budget violations block the query or log a warning. Strict mode always blocks |
severities.latency | 'warn' | 'error' | 'warn' | Whether an over-budget latency blocks the query or logs a warning. Strict mode always blocks |
A latency violation blocks when severities.latency is 'error', or when the runtime is in strict mode. The postgres(...) client is always strict: it never forwards a mode, so BUDGET.TIME_EXCEEDED throws there whatever you set. Only mongo(...) accepts mode: 'permissive', and there the default 'warn' logs instead of throwing. Either way the check happens after execution, so it is a visibility tool rather than a cancellation mechanism.
How it enforces the budget
The middleware checks at three points in the query lifecycle:
- Before execution (in
beforeQueryfor row queries,beforeExecutefor writes), it inspects the query before it runs. ASELECTwith noLIMIT(and no aggregation that collapses the result) is treated as unbounded, and a bounded query whose estimated rows exceedmaxRowsis over budget. The estimate comes fromtableRowsanddefaultTableRows, so tune those to your data. Violations raiseBUDGET.ROWS_EXCEEDEDbefore the driver runs, or warn whenseverities.rowCountis'warn'and the runtime is not strict. - As rows arrive (in
onRow), it counts what the database actually returns. If the observed count passesmaxRows, it throwsBUDGET.ROWS_EXCEEDEDand aborts the stream, which protects you when the estimate was wrong. - After execution (in
afterQueryandafterExecute), it compares the measuredlatencyMsagainstmaxLatencyMsand reportsBUDGET.TIME_EXCEEDEDwhen the query ran too long. This check runs after the work is done, so it cannot save the slow query itself; it exists to make slow queries loud instead of invisible.
What you see when a budget trips
A blocked query rejects with a runtime error carrying the budget code and details:
BUDGET.ROWS_EXCEEDED: Unbounded SELECT query exceeds budget
{ source: 'ast', maxRows: 10000 }The source field tells you whether the violation came from the pre-execution estimate ('ast') or the observed rows ('observed'). A blocking latency violation carries the measured and allowed values:
BUDGET.TIME_EXCEEDED: Query latency exceeds budget
{ latencyMs: 2481, maxLatencyMs: 1000 }When the violation only warns, the same information is emitted through the runtime logger. To fix an unbounded-select violation, add a limit to the query (limit(...) on the ORM API, .limit(...) on the SQL query builder) or raise the budget deliberately.
Common gotchas
The pre-execution row check estimates from tableRows and defaultTableRows, not from live table statistics. If your real tables are much larger than the configured assumptions, a query can pass the estimate and still hit the observed-row limit mid-stream. Keep tableRows roughly honest for the tables you query most.
See also
- How middleware works
- Built-in: lints for structural query checks that complement budgets
- Authoring custom middleware
