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

> 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 cache middleware serves repeated reads from an in-memory store, opted in per query with a cache annotation.

Location: ORM > Middleware > Built-in: cache

A middleware is an object you register on your client that Prisma ORM calls around every query your app runs. The cache middleware keeps the rows a read returned in memory, so when your app runs that same read again it gets the stored rows back instead of going to the database. Nothing is cached unless you ask for it: you mark a query with a TTL, meaning how long its stored rows stay usable, and every query you do not mark runs against the database exactly as it did before.

Reach for it when the same read runs over and over and you can live with an answer that is a little out of date, such as a dashboard, a lookup table, feature flags, navigation data, or a search result that is expensive to run.

Install the package and register the middleware:

  

#### bun

```bash title="Terminal"
bun add @prisma/orm-extension-middleware-cache
```

#### pnpm

```bash title="Terminal"
pnpm add @prisma/orm-extension-middleware-cache
```

#### yarn

```bash title="Terminal"
yarn add @prisma/orm-extension-middleware-cache
```

#### npm

```bash title="Terminal"
npm install @prisma/orm-extension-middleware-cache
```

Your contract is the Prisma ORM 8 name for your schema: `contract.prisma` in a generated project, in place of `schema.prisma`. `npx prisma contract emit` writes `contract.json` and `contract.d.ts` from it, and those are the two files `db.ts` imports.

```ts title="src/prisma/db.ts"
import { createCacheMiddleware } from '@prisma/orm-extension-middleware-cache';
import postgres from '@prisma/orm-postgres/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: [createCacheMiddleware()],
});
```

This is a cut-down `src/prisma/db.ts` from a generated project, with `middleware` added. A project created with `npm create prisma@latest` already has `contract.json` and `contract.d.ts` in `src/prisma/`, so you do not have to run `npx prisma contract emit` yourself to get started. If you set your project up by hand, [The data contract](https://www.prisma.io/docs/orm/contract-authoring/the-data-contract) covers where they come from.

Writes are never cached, and the `*AndCount` methods return a count rather than rows, so there is nothing to cache in their case either.

The cache works on PostgreSQL and on MongoDB. You register it on MongoDB the same way: pass `createCacheMiddleware()` in the `middleware` option of `mongo(...)`, which comes from `@prisma/orm-mongo/runtime`.

## Opt a query in [#opt-a-query-in]

You tell Prisma ORM that a read may be cached by attaching a cache annotation to it. An annotation is a note attached to a query that middleware can read, and the one the cache looks for is `cacheAnnotation({ ttl })`, where `ttl` is how long the rows stay usable, in milliseconds. There is no option that caches every read for you, so a read is cached only when it has an annotation of its own.

Prisma ORM gives you two ways to write the same read, the SQL query builder and the ORM API, and both of them can be cached. Use whichever one the rest of your code already uses, because the annotation attaches differently in each.

With the SQL query builder you build the query yourself, call `.build()`, and run the result with `db.runtime().query(...)` for rows or `db.runtime().execute(...)` for a count. Only rows can be cached, so nothing you run with `db.runtime().execute(...)` ever is. To cache a read, chain `.annotate(cacheAnnotation({ ttl: 60_000 }))` onto the query before you call `.build()`. `db.sql.public.user` names the `user` table in the PostgreSQL schema `public`.

```ts title="src/prisma/get-users-cached.ts"
import { cacheAnnotation } from '@prisma/orm-extension-middleware-cache';
import { db } from './db';

export async function getUsersCached() {
  const plan = db.sql.public.user
    .select('id', 'email')
    .annotate(cacheAnnotation({ ttl: 60_000 }))
    .limit(10)
    .build();

  return db.runtime().query(plan);
}
```

The ORM API does both steps inside the one call, and `db.orm.public.User` is the `User` model in the PostgreSQL schema `public`, rather than the table it is stored in. Its reads take an optional callback after the arguments, and the `meta` object that callback receives is where annotations go:

```ts title="src/prisma/get-user-cached.ts"
import { cacheAnnotation } from '@prisma/orm-extension-middleware-cache';
import { db } from './db';

export async function getUserCached(id: number) {
  return db.orm.public.User.first({ id }, (meta) =>
    meta.annotate(cacheAnnotation({ ttl: 60_000 })),
  );
}
```

The first call goes to the database as usual, and the cache keeps the rows it got back, exactly as they came. If the same query with the same parameter values runs again before the `ttl` has run out, Prisma ORM answers it from those stored rows and never calls the database driver.

You can set the following fields on the annotation:

| Field  | Type      | What it does                                                                     |
| ------ | --------- | -------------------------------------------------------------------------------- |
| `ttl`  | `number`  | How long the stored rows stay usable, in milliseconds                            |
| `skip` | `boolean` | Set it to `true` to send this one call to the database, ignoring anything stored |
| `key`  | `string`  | Your own string to store the rows under, instead of the one the cache works out  |

Leave `ttl` out and the annotation does nothing, so the query is not cached.

`skip` and `key` go in the same object as `ttl`, in either API. `skip: true` sends that one call to the database and leaves the stored rows untouched, so everyone else keeps getting the stored rows until the `ttl` runs out. The `ttl` in the same object is ignored on a call with `skip: true`, which is why you can leave the annotation as it is and flip `skip` on the one call that has to see current rows. With the SQL query builder it goes in the same object you pass to `.annotate(...)`:

```ts
const plan = db.sql.public.user
  .select('id', 'email')
  .annotate(cacheAnnotation({ ttl: 60_000, skip: true }))
  .limit(10)
  .build();

return db.runtime().query(plan);
```

A `key` is used exactly as you wrote it, and the cache adds nothing to it, so any two reads that pick the same `key` share one entry even when their parameter values or their models differ: the rows stored for `id: 1` come back for `id: 2` as well. Either use `key` only on a query whose parameter values never change, or put the parameter values into the string yourself, behind a prefix no other read in your app uses:

```ts
return db.orm.public.User.first({ id }, (meta) =>
  meta.annotate(cacheAnnotation({ ttl: 60_000, key: `user:${id}` })),
);
```

Attaching `cacheAnnotation` to a write such as `create`, `update`, or `delete` does not compile: TypeScript rejects it as you write the code, and forcing it through throws when the query runs. You cannot cache a write by accident.

## When a read is served from the cache [#how-keys-and-hits-work]

Unless you give it a `key`, the cache stores a read under a key built from the query, its parameter values, and a hash of your contract, and it serves a read from the cache only when all three match. The same read with a different parameter value is therefore a separate entry and does not get the first one's rows.

That contract hash changes with any migration, whichever table the migration touched, so after a migration every entry the cache keyed for itself stops matching and none of them are served again. An entry you stored under your own `key` is not affected, because your `key` is used exactly as you wrote it and has nothing about your contract in it.

Your own calling code cannot see the difference, because `db.runtime().query(plan)` and `db.orm.public.User.first(...)` give you the same rows whether the cache answered or the database did.

A read inside `db.transaction(...)`, or on a connection you took out of the pool yourself with [`db.runtime().connection()`](https://www.prisma.io/docs/orm/reference/transactions-and-runtime#manual-connection-and-transaction-control), always goes to the database, and its rows are not stored. Code that holds a transaction or a connection expects to read back what it has just written, so the cache stays out of the way and you always see your own writes.

## Check that caching is working [#check-that-caching-is-working]

Prisma ORM has no counter or log option that reports cache hits, so to see them, register a small middleware of your own after the cache. A read the cache answers still reaches the middleware after it, so your own logging and metrics keep recording it and a hook like the one below runs either way. That hook is `afterQuery`, and the `result` argument it gets says who answered: `result.source` is `'middleware'` when the cache answered and `'driver'` when the database did.

```ts title="src/prisma/cache-watch.ts"
import type { SqlMiddleware } from '@prisma/orm-postgres/family-runtime';

export function cacheWatch(): SqlMiddleware {
  return {
    name: 'cache-watch',
    async afterQuery(plan, result) {
      console.log(`[cache-watch] ${result.source} · ${plan.sql}`);
    },
  };
}
```

The other built-in middleware and the `SqlMiddleware` type are imported from `@prisma/orm-postgres/family-runtime`, which works with any SQL database. On MongoDB the same middleware is typed `MongoMiddleware`, from `@prisma/orm-mongo/family-runtime`, and a MongoDB query has no `sql` field, so log `plan.command` there instead.

Register `cacheWatch()` after the cache, by replacing the `middleware` line in `src/prisma/db.ts` and adding the import above it. Order matters for a middleware that returns rows itself instead of letting the query reach the database, because the first one to do that wins and the ones after it are never asked. `cacheWatch()` only logs, and a middleware of your own that can return rows goes after the cache as well, so that the cache is asked first.

```ts title="src/prisma/db.ts (excerpt)"
import { cacheWatch } from './cache-watch';

middleware: [createCacheMiddleware(), cacheWatch()],
```

Now call the same annotated read twice. The first call prints `driver`, because it went to the database, and the second prints `middleware`, because the cache answered it. If you register other middleware of your own that can return rows itself, `result.source` is `'middleware'` for all of them, so it tells you that a middleware answered and not which one.

## When entries go away [#when-entries-go-away]

There is no call that clears the cache or removes one entry. An entry stops being served when its `ttl` runs out, or when the built-in store drops its least recently used entry to stay under `maxEntries`.

Writes do not clear anything either. A `create`, `update`, or `delete` leaves every stored entry exactly where it was, so a read with a `ttl` of 60 seconds can keep returning the old rows for up to a minute after you have changed them. You have two ways to live with that, and no third: pick a `ttl` you can afford to be wrong for that long, and send the calls that have to see the change straight away with `skip: true`. A read that can never be even a second out of date is a read you should not annotate at all.

## Options [#options]

These are the options you pass to `createCacheMiddleware`:

| Option       | Type           | Default                      | What it controls                                                                        |
| ------------ | -------------- | ---------------------------- | --------------------------------------------------------------------------------------- |
| `maxEntries` | `number`       | `1000`                       | How many entries the built-in store holds before it drops its least recently used entry |
| `store`      | `CacheStore`   | the built-in in-memory store | Where the cached rows are kept, for example in Redis                                    |
| `clock`      | `() => number` | `Date.now`                   | The function the cache calls for the current time, recorded on each entry               |

`maxEntries` applies to the built-in store only. When you pass your own `store`, the cache ignores `maxEntries` and your store decides for itself how much it holds.

## Common gotchas [#common-gotchas]

> [!WARNING]
> The built-in store holds the rows in the memory of one process, so if you run two instances of your app each one keeps its own copy of the rows and fills it at its own pace, and every deploy starts both of them empty again. Two requests that run the same read can therefore get different rows depending on which instance served them, so never let a decision that has to be right rest on a cached read, such as a permission check or an account balance. Send those reads with `skip: true`, or leave them unannotated.

## Share one cache between instances [#share-one-cache-between-instances]

To have every instance read from the same cache, pass your own `store`. A store is an object with two methods, both async, so it can talk to Redis or to anything else your instances can all reach. `get(key)` takes a string and returns the entry stored under it, or `undefined` when there is nothing for that key. `set(key, entry, ttlMs)` keeps an entry under that key for `ttlMs` milliseconds.

Working out that an entry has expired is your store's job, so your `get` must not return an entry whose `ttlMs` has passed. A store backed by Redis can let Redis's own expiry do that for you, by setting it when `set` is called.

An entry is an object with two fields, `{ rows, storedAt }`. `storedAt` is the time the cache recorded when it stored the rows, taken from the `clock` option. `rows` is the rows as the database driver returned them, so integers are numbers and both text and timestamps are strings. A `createdAt` column reaches your store as a string and not a `Date`, as a row from `select('id', 'email', 'createdAt')` shows:

```ts
{ id: 3, email: 'mia@prisma.io', createdAt: '2026-09-17 08:00:08.775+00' }
```

Your store gets those objects as they are and has to hand back the same thing from `get`, so turning them into something Redis can hold, and back again, is yours to write. Log a row before you write the store, so you can see the values for your own columns.

```ts title="src/prisma/cache-store.ts"
import type { CacheStore } from '@prisma/orm-extension-middleware-cache';

export const store: CacheStore = {
  async get(key) {
    // return { rows, storedAt } when you have unexpired rows for this key, otherwise undefined
    return undefined;
  },
  async set(key, entry, ttlMs) {
    // keep entry under key for ttlMs milliseconds
  },
};
```

Register the middleware with your store instead of `maxEntries`, again by replacing the `middleware` line in `src/prisma/db.ts`:

```ts title="src/prisma/db.ts (excerpt)"
import { store } from './cache-store';

middleware: [createCacheMiddleware({ store })],
```

## See also [#see-also]

* [How middleware works](https://www.prisma.io/docs/orm/middleware/how-middleware-works)
* [Authoring custom middleware](https://www.prisma.io/docs/orm/middleware/authoring-custom-middleware), including how a middleware answers a query itself instead of letting it reach the database
* [Built-in: budgets](https://www.prisma.io/docs/orm/middleware/built-in-budgets)

## 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: budgets`](https://www.prisma.io/docs/orm/middleware/built-in-budgets): The budgets middleware caps row counts and reports queries that took too long.
- [`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.