For the complete Prisma documentation index, see llms.txt. A markdown version of any docs page is available by appending .md to its URL.
The budgets middleware caps row counts and reports queries that took too long.
Location: ORM > Middleware > Built-in: budgets
The budgets middleware stops one careless query from reading far more of your data, or taking far more time, than you meant to give it. You give it a row limit and a time limit, and it checks the queries you send against both. When a query goes over either limit, your call fails with an error and you get no rows back.
Before you register it, know that budgets rejects any SELECT from the ORM API or the SQL query builder that has no LIMIT on it, however few rows the table holds. A statement you wrote yourself with db.raw.sql is not checked this way. On the ORM API, the call that runs a query and gives you every matching row is .all(), so that means every .all() call with no .limit(...) earlier in the chain. A .where(...).first() call is fine, because .first() asks the database for one row.
Those .all() calls start throwing the moment you add budgets to your client, and there is no warn-only setting to ease the change in. The severities options in the table below look like one, but they do nothing on a client you build with postgres(...). So find your .all() calls and give them limits before you turn budgets on, and searching your source for the call is a good starting point:
Bash
grep -rn "\.all()" src scripts
The fix is a .limit(...) in the chain. A query that counts rows is never a problem, because a count that is not split into groups comes back as a single row. In the calls below, db.orm is the ORM API and public is the PostgreSQL schema your tables are in:
TypeScript
// Throws an error whose code is BUDGET.ROWS_EXCEEDED, because there is no limit.await db.orm.public.User.select('id').all();
// Runs, because the query asks for at most 20 rows.await db.orm.public.User.limit(20).all();
// Runs, because a count that is not split into groups returns one row.await db.orm.public.User.aggregate((a) => ({ total: a.count() }));
Register budgets in the postgres(...) call that creates your client, with the row and time limits you want to enforce. The file below is the smallest src/prisma/db.ts that works, and the middleware option is the only part specific to budgets. The db.ts that create-prisma generates has more in it, including a connectDatabase export that the generated seed.ts imports, so add the middleware option to that file rather than replacing it. Its postgres(...) calls only run when the app is started with DATABASE_URL exported, as npm run dev does. With npm run dev:composer, the generated file takes its client from Composer through service.load() instead, and the middleware option in db.ts is never used, so budgets is not on. Your contract is the Prisma ORM 8 name for your schema, contract.prisma in place of schema.prisma, and npx prisma contract emit writes from it the contract.json and contract.d.ts that this file imports. The data contract covers them in full.
The built-in middleware is imported from @prisma/orm-postgres/family-runtime, which works with any SQL database. It arrives with @prisma/orm-postgres, so there is nothing extra to install.
The example sets maxRows, defaultTableRows, and maxLatencyMs to the values they already take by default, so that you can see where each one goes, and you can leave all three out. Set maxRows and maxLatencyMs to the largest number of rows and the longest time any single query in your app should ever need, because they are there to catch mistakes rather than to tune anything.
Every SELECT needs a LIMIT anyway, but a limit can still be larger than maxRows. The check before the query knows how big a table is only from the number you give it in tableRows, so with a real number there, a query like that on a big table fails before it runs rather than part way through reading its rows, as Common gotchas shows. The two numbers in the example are placeholders for how many rows your own tables really hold, so a Post table with two million rows in it is written tableRows: { Post: 2_000_000 }. A count from the database is enough to get each number, such as select count(*) from "public"."User"; for the User table, and you can leave tableRows out altogether, in which case every table is assumed to hold defaultTableRows rows.
Each key is the table name as it is in the database: the model's @@map value, or the model name exactly as written when the model has no @@map. So the model User is keyed User, not public.User. budgets looks a table up by its name alone, so two tables with the same name in different PostgreSQL schemas share one key and one number. If you misspell a table name, budgets silently uses defaultTableRows for that table and never warns you.
Passing budgets to a MongoDB client throws an error whose code is RUNTIME.MIDDLEWARE_FAMILY_MISMATCH the first time that client is used, before any query of yours runs, and there is no MongoDB version of budgets to use instead.
A query is checked before it is sent, again as its rows arrive, and once more when it is done:
Before the query runs, budgets estimates how many rows the query could return, and when the estimate is over maxRows you get an error whose code is BUDGET.ROWS_EXCEEDED before the database is asked to do anything. A SELECT with no LIMIT on it is over budget on its own, whatever maxRows says and however small the table is. The exception is a query that can only ever produce one row, such as the count above.
While the rows come back, budgets counts the rows the database really returns, so a query that got past the estimate is still caught. As soon as that count passes maxRows you get an error whose code is BUDGET.ROWS_EXCEEDED, and the rows read up to that point are dropped rather than returned to you.
After the query finishes, budgets compares how long the query took against maxLatencyMs and throws an error whose code is BUDGET.TIME_EXCEEDED when it ran too long. The database has already done the work by then, so the check cannot make the query faster, but your await still rejects and you get no rows.
A query that joins another table is estimated from the table in its FROM clause alone, because that is the only name budgets looks up in tableRows, so do not count on the first check to catch a join. Writes are not estimated at all: the check before the query reads SELECT queries only, so a write that returns a count, such as an updateAndCount that reports how many rows it changed, can only ever be caught by the time check. A read with .include(...), which loads related records alongside the rows you asked for, is one query, checked once like any other read.
When budgets blocks a query, the call you were awaiting rejects with an ordinary Error that has a code property saying which budget was exceeded and a details property with the numbers it measured. If you do not catch it, a SELECT with no LIMIT and maxRows at 10_000 prints this, where "unbounded" in the message is the error's own word for a query with no LIMIT:
The source field tells you which of the two row checks stopped the query. 'ast' is the check before the query ran, and 'observed' means the database really returned that many rows. What you do about it is the same either way, which is to give the query a limit, but 'observed' also tells you the table is much bigger than the number you gave for it in tableRows.
When a query runs longer than maxLatencyMs, the error gives you both numbers, the time the query took and the time you allowed:
Both errors are plain Error objects, so you catch them the way you catch anything else and read the code to tell which budget was exceeded:
TypeScript
try {
await db.orm.public.User.select('id').all();
} catch (error) {
if ((error as { code?: string }).code === 'BUDGET.ROWS_EXCEEDED') {
// add a limit, or raise the budget
}
throw error;
}
Most of the time the fix is a limit, with limit(...) on the ORM API or .limit(...) on the SQL query builder. When the query is right and the budget is too low, raise maxRows, but that raises it for every query on that client, because budgets is an option of the client and no query can opt out of it.
So for a job that genuinely has to read millions of rows, such as a nightly export, build a second client from the same contract and leave budgets out of it:
Import that db in your export script and nowhere else, so the rest of your app keeps importing src/prisma/db.ts and keeps its budgets. Each postgres(...) call opens its own connection pool, so while the job runs you have a second pool open alongside your app's.
[!WARNING]
The check that runs before your query never asks the database how big your tables are. It only knows the numbers you gave it in tableRows and defaultTableRows, so when your real tables are much larger than those numbers, a query can get past that first check and then fail part way through reading rows, once the rows the database returns pass maxRows.
Say maxRows is 10_000 and you listed User at 5,000 rows, but the table really holds 5 million. A query with .limit(50_000) on it is estimated at 5,000 rows, because budgets takes the smaller of your limit and the size you gave for the table, so it passes the first check. The database then starts sending 50,000 rows and the count check throws at row 10,001, part way through your read.
When a query that used to work starts failing with source: 'observed', the table has grown past the number you gave for it in tableRows. Put a fresh count there, and the check before the query then stops that query with source: 'ast' before the database does any work, rather than part way through the read. The query is still over budget either way, because its limit is larger than maxRows. To make it run, raise maxRows, or lower its .limit(...) to maxRows or less and read the rest a page at a time with .offset(...), as Sort and paginate describes: