# Built-in: budgets (/docs/orm/middleware/built-in-budgets)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/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](https://www.prisma.io/docs/orm/fundamentals/reading-data), 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:

```ts
// 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](https://www.prisma.io/docs/orm/contract-authoring/the-data-contract) covers them in full.

```ts title="src/prisma/db.ts"
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,
    }),
  ],
});
```

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.

The two numbers in `tableRows` 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, which is the model name with a lowercase first letter, so the model `User` is keyed `user` and not `User` or `public.User`. If you renamed a table with `@@map` in your contract, use the name you mapped it to. 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.

## Options [#options]

Every option is optional, and the default in the table applies when you leave one out.

| Option                | Type                     | Default   | What it controls                                                                                   |
| --------------------- | ------------------------ | --------- | -------------------------------------------------------------------------------------------------- |
| `maxRows`             | `number`                 | `10_000`  | The most rows one query may produce                                                                |
| `defaultTableRows`    | `number`                 | `10_000`  | How many rows to assume a table has when it is not listed in `tableRows`                           |
| `tableRows`           | `Record<string, number>` | `{}`      | How many rows to assume each table has, keyed by table name                                        |
| `maxLatencyMs`        | `number`                 | `1_000`   | The longest one query may take, in milliseconds                                                    |
| `severities.rowCount` | `'warn' \| 'error'`      | `'error'` | Nothing on a client you build with `postgres(...)`, where going over the row budget always throws  |
| `severities.latency`  | `'warn' \| 'error'`      | `'warn'`  | Nothing on a client you build with `postgres(...)`, where going over the time budget always throws |

`severities` has no effect on a client you build with `postgres(...)`, where both budgets always throw. You can leave it out.

## How it enforces the budget [#how-it-enforces-the-budget]

A query is checked before it is sent, again as its rows arrive, and once more when it is done:

1. **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.
2. **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.
3. **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.

## What you see when a budget trips [#what-you-see-when-a-budget-trips]

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`:

```text title="Row budget error" no-copy
Error [RuntimeError]: Unbounded SELECT query exceeds budget
    ... {
  code: 'BUDGET.ROWS_EXCEEDED',
  category: 'BUDGET',
  severity: 'error',
  details: { source: 'ast', estimatedRows: 10000, maxRows: 10000 }
}
```

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:

```text title="Latency budget error" no-copy
Error [RuntimeError]: Query latency exceeds budget
    ... {
  code: 'BUDGET.TIME_EXCEEDED',
  category: 'BUDGET',
  severity: 'error',
  details: { latencyMs: 1501, maxLatencyMs: 1000 }
}
```

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:

```ts
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](https://www.prisma.io/docs/orm/fundamentals/reading-data#sort-and-paginate) or `.limit(...)` on the [SQL query builder](https://www.prisma.io/docs/orm/reference/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:

```ts title="scripts/export/db.ts"
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from '../../src/prisma/contract.d';
import contractJson from '../../src/prisma/contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL']!,
});
```

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.

## Common gotchas [#common-gotchas]

> [!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. A query that used to work failing with `source: 'observed'` is your sign to put a fresh count into `tableRows`.

## See also [#see-also]

* [How middleware works](https://www.prisma.io/docs/orm/middleware/how-middleware-works)
* [Built-in: lints](https://www.prisma.io/docs/orm/middleware/built-in-lints) for structural query checks that complement budgets
* [Authoring custom middleware](https://www.prisma.io/docs/orm/middleware/authoring-custom-middleware)

## Related pages

- [`Authoring custom middleware`](https://www.prisma.io/docs/orm/middleware/authoring-custom-middleware): Build, register, and run your own Prisma ORM middleware, step by step, starting with a query logger.
- [`Built-in: cache`](https://www.prisma.io/docs/orm/middleware/built-in-cache): The cache middleware serves repeated reads from an in-memory store, opted in per query with a cache annotation.
- [`Built-in: lints`](https://www.prisma.io/docs/orm/middleware/built-in-lints): The lints middleware inspects each query's structure before it runs and blocks or warns on risky shapes.
- [`How middleware works`](https://www.prisma.io/docs/orm/middleware/how-middleware-works): Middleware runs your code before and after every query, so one policy can cover your whole app.