cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Private and remote caches

Cache per-user results with 'use cache: private', share results across servers with 'use cache: remote' and cacheHandlers, and keep personal data safe.

By the end, you can
  • 01
    Cache a visitor's own dataUse 'use cache: private' to read cookies inside a cached function, and say where its result is kept: one request on the server, and the browser.
  • 02
    Share results across serversUse 'use cache: remote' for data many visitors share, and give it a store with cacheHandlers, knowing what happens without one.
  • 03
    Keep personal data out of shared storesSpot per-user data on its way into a shared cache, read what such a store holds, and choose the cache that fits each part of a page.

Orders that belong to one visitor

The 'use cache' directive stores what a function returns, and gives the same inputs the same stored copy. That copy is shared by every request, so a plain "use cache" function can't read cookies(): reading it there is an error. The tea shop's orders page needs exactly that. It shows the signed-in visitor's own orders, and the shop knows who is visiting from a session cookie: a cookie that holds a session id, a value the shop gave the visitor when they signed in.

Next.js has two more versions of the directive. "use cache: private" is for results that belong to one visitor. "use cache: remote" is for results that many visitors share, kept in a store of their own, which every server can share once a cache handler, or your hosting platform, supplies one. Here is the private one. It's a real Next.js 16.4.3 project, built with next build, run with next start, and recorded:

A visitor's orders, from a private cacheNext.js
app/orders/sign-in/lib/
lib/orders.ts
import { cookies } from "next/headers"; const api = process.env.SHOP_API_URL; export type Order = { id: number; tea: string; boxes: number }; export async function getMyOrders(): Promise<Order[]> {  "use cache: private";  const session = (await cookies()).get("session")?.value;  const response = await fetch(`${api}/orders?session=${session}`);  return response.json();}
Recorded from Next.js 16.4.3
localhost:3000/ordersTea shop
Home
Your orders

You have 2 orders.

  • 2 boxes of Mint tea
  • 3 boxes of Green tea

Sent with cookie: session=asha-7f3k. The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

In lib/orders.ts, the file shown first, getMyOrders on lines 7 to 12 asks the shop's API for the visitor's orders. Line 8, "use cache: private", makes it a private cache. Line 9 reads the session cookie inside the cached function, which a plain "use cache" doesn't allow. Line 10 sends the session to the API. Open app/orders/page.tsx: OrderCount on lines 16 to 19 and OrderList on lines 21 to 32 both call getMyOrders, inside the Suspense on lines 8 to 11.

The API is a stand-in that ran on the same computer and kept every request it received. Three visits came in: Asha, Asha again, and Ben, each with their own session cookie. Pick each one above the page in the Browser tab. Asha sees 2 boxes of Mint tea and 3 boxes of Green tea; Ben sees his own orders. The three visits caused three API calls, one each, even though two components asked on each visit. (The recording's prefetches, further down, asked again.) The Terminal shows ◐ /orders: the heading is in the static shell, and the orders stream in for each request.

Three directives on the left, three places on the right. use cache goes to each server's own memory. use cache: remote, with a handler, goes to a store all servers share. use cache: private goes to the visitor's browser, and on the server lasts only one request."use cache""use cache: remote""use cache: private"serverits own memorystoreshared by all serversbrowserthis visitor onlywith a handlerone request on the server
Where each directive keeps what it stores, in production. "use cache" uses each server's memory. "use cache: remote" uses a shared store once a handler is configured, and the same memory when none is. "use cache: private" keeps nothing on the server after the request, and the visitor's browser keeps what the router fetched.
Real-life analogyA shelf, a storeroom and a tray

A tea chain has three places to keep things. Tins any customer may buy go on a shelf behind the counter, and each branch has its own shelf. Big orders go in a storeroom every branch shares. A customer's own order is written on a note on their tray, and the staff throw the note away when the order is served. The customer leaves with a receipt, which nobody else sees.

In real life: A shelf behind the counter of one branch, for things any customer may get
In Next.js: "use cache" with the built-in handler: each server's own memory
In real life: The storeroom every branch of the chain shares
In Next.js: "use cache: remote" with a configured handler
In real life: A note on one customer's tray, thrown away when the order is done
In Next.js: "use cache: private" on the server: one request
In real life: The receipt the customer takes home
In Next.js: What the router keeps in that visitor's browser
The main idea

"use cache: private" caches a result that belongs to one visitor; "use cache: remote" caches a result many visitors share, in a store all servers can share once a handler, or your host, provides one. A private cache may read cookies(), headers() and searchParams, and keeps nothing on the server after the request. A remote cache can't read them, and its store is set in cacheHandlers.

What a private cache reads, and where it keeps results

A private cache is allowed to read request data that no shared cache may read. Each row is one request-time API (Request-time APIs teaches them), as Next.js's docs list them:

What each cache may read inside it
APIInside "use cache" or "use cache: remote"Inside "use cache: private"
cookies()No. The build stops: read it outside and pass the value in.Yes.
headers()No, for the same reason.Yes.
searchParamsNo, for the same reason.Yes.
connection()No.No. It's about one connection, which no cache may keep.

On the server: one request

The three visits asked the API three times. So where did the private result go? In production, nowhere that lasts. Next.js keeps it only for the request that made it. The docs say calls with the same inputs during one request can share one result, and the recording fits: OrderCount and OrderList both asked, and the API answered once per visit. When the response is sent, the result is gone from the server. In Next.js's own code, the private branch picks no cache handler at all:

The start of the private branch, from use-cache-wrapper.ts at v16.4.3TypeScript
if (isPrivate) {
  // Private caches normally go to the Resume Data Cache (RDC), not a cache
  // handler. In development we additionally persist them in a dedicated
  // built-in in-memory handler so that reloads are fast.
  if (process.env.__NEXT_DEV_SERVER) {
    cacheHandler = getPrivateCacheHandler()
  }

Line 1 checks for a private cache. Lines 2 to 4 say where its result goes instead: the Resume Data Cache, a store that belongs to one render of one request. Lines 5 to 7 add one exception, for next dev only: there, Next.js also keeps private results in memory so a reload is fast. The docs say that development store is keyed by the request's cookies and headers, so a visitor with other cookies gets another entry. (These are lines 1935 to 1941, and the dev store is made in handlers.ts, lines 126 to 133.)

The orders are also left out of the static shell. In the Network tab above, the HTML for each visit starts with Loading your orders…, and the orders arrive later in the same response. The response's Cache-Control: private, no-cache, no-store… tells every cache between the server and the browser, like a CDN, not to keep it.

In the browser: a prefetch made for one visitor

The private result has one more place to live: the visitor's browser. These steps were taken in real Chrome against the same app. Asha signs in (the shop's stand-in sign-in sets her session cookie and sends her to /), waits a moment, and clicks Your orders. Open the Steps tab:

Signing in as Asha, then opening Your ordersNext.js
app/orders/sign-in/lib/
lib/orders.ts
import { cookies } from "next/headers"; const api = process.env.SHOP_API_URL; export type Order = { id: number; tea: string; boxes: number }; export async function getMyOrders(): Promise<Order[]> {  "use cache: private";  const session = (await cookies()).get("session")?.value;  const response = await fetch(`${api}/orders?session=${session}`);  return response.json();}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/ordersTea shop
Home Your orders
Your orders

You have 2 orders.

  • 2 boxes of Mint tea
  • 3 boxes of Green tea
Focus
the link “Your orders”
Scrolled
0 px down

No requests during this step: the page already had what it needed

  1. Steps 1 and 2. The home page loads with the Your orders link on the screen, and the router prefetches the orders page. One of its requests has the header next-router-prefetch: 3: in Next.js 16.4.3, that's the router asking for a prefetch rendered on the server for this visitor, with her cookie. Step 2 waits until that request has answered.
  2. Step 3, the click. The orders appear, and the step made no request at all: everything came with the prefetch.
  3. Steps 4 and 5. Home, then Your orders again. Still no request: the router showed the orders from what it kept in the browser's memory.

The recorder also sent that prefetch on its own, twice, with the same headers Chrome sent: once with Asha's cookie and once with Ben's. In the Network tab, pick the prefetch for Asha, then the prefetch for Ben. Each answer is an RSC payload (the description of the rendered page that React uses on a client-side navigation), and each holds only that visitor's orders: "Mint tea" and "Green tea" for Asha, "Lemon tea" and "Mint tea" for Ben. The docs call this a route's App Shell, the part a link prefetches; for a route that reads cookies, it's made for each session and kept on the client for that session. Instant navigations and partial prefetching takes prefetching apart.

How long may the browser keep it? The docs say the router can keep a private result for its stale time, set with cacheLife (Cache lifetimes and tags teaches it). getMyOrders doesn't call cacheLife, so it gets the default profile's 5 minutes. Below 30 seconds, it isn't prefetched at all; below 5 minutes, it isn't part of the App Shell. A reload clears the router's memory. And unlike the other two kinds, a private cache can't be given a store of your own.

Per-user data in a shared cache

A shared cache serves one stored copy to every request with the same key. Personal data has no business there. Next.js stops the most direct mistake. This version marks getMyOrders with "use cache: remote" and keeps the cookie read inside:

A remote cache that reads the session cookie (an error on purpose)Next.js
app/orders/sign-in/lib/
lib/orders.ts
import { cookies } from "next/headers"; const api = process.env.SHOP_API_URL; export type Order = { id: number; tea: string; boxes: number }; export async function getMyOrders(): Promise<Order[]> {  "use cache: remote";  const session = (await cookies()).get("session")?.value;  const response = await fetch(`${api}/orders?session=${session}`);  return response.json();}
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...⨯ Error: Route "/orders": `cookies()` can't be read inside `"use cache"`. Read it outside the cached function and pass what you need as an argument.Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache    at g (lib/orders.ts:9:26)   7 | export async function getMyOrders(): Promise<Order[]> {   8 |   "use cache: remote";>  9 |   const session = (await cookies()).get("session")?.value;     |                          ^  10 |   const response = await fetch(`${api}/orders?session=${session}`);  11 |   return response.json();  12 | } {  digest: '1364005679'}Error: Route "/orders": `cookies()` can't be read inside `"use cache"`. Read it outside the cached function and pass what you need as an argument.Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache    at g (lib/orders.ts:9:26)   7 | export async function getMyOrders(): Promise<Order[]> {   8 |   "use cache: remote";>  9 |   const session = (await cookies()).get("session")?.value;     |                          ^  10 |   const response = await fetch(`${api}/orders?session=${session}`);  11 |   return response.json();  12 | } {  digest: '1364005679'}To get a more detailed stack trace and pinpoint the issue, try one of the following:  - Start the app in development mode by running `next dev`, then open "/orders" in your browser to investigate the error.  - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/orders". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /orders/page: /orders, exiting the build.⨯ Next.js build worker exited with code: 1 and signal: null

The build stopped at line 9: Route "/orders": cookies() can't be read inside "use cache". Read it outside the cached function and pass what you need as an argument. The message says "use cache" for the remote version too: both are shared caches, with the same rule.

The message suggests a way around the rule, and it works: read the cookie outside, and pass the session in. Then the key holds the session, so Asha and Ben get separate entries, and nobody receives somebody else's orders by accident. But look at what the shared store now holds. Both versions below use the same small cache handler: a file, set in next.config.ts, that prints everything Next.js hands it. Pick a version, then read the Terminal under $ next start:

Playground: who keeps Asha's orders?
The orders page, with this version of lib/orders.tsNext.js
app/orders/sign-in/cache-handlers/lib/
lib/orders.ts
import { cookies } from "next/headers"; const api = process.env.SHOP_API_URL; export type Order = { id: number; tea: string; boxes: number }; export async function getMyOrders(): Promise<Order[]> {  "use cache: private";  const session = (await cookies()).get("session")?.value;  const response = await fetch(`${api}/orders?session=${session}`);  return response.json();}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (5/5)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ◐ /orders└ ƒ /sign-in ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed contentƒ  (Dynamic)            server-rendered on demand $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready
Try it yourself
'use cache: private'

Line 8 makes getMyOrders a private cache, and line 9 reads the session cookie inside it. The project has the same printing handler, but the Terminal shows no remote get or remote set under next start: a private cache never reaches a cache handler. Three visits asked the API three times, because nothing was kept on the server between requests.

Both versions were built with next build from Next.js 16.4.3, with the same printing remote handler, and visited by Asha, Asha again, and Ben, each with a session cookie. After the build, the Terminal shows what next start printed. Reset goes back to the first version.

With the remote version, the store received this from the first visit (the build's and function's IDs shortened here):

What the shared store received for AshaText
remote set: ["c0fb…",["asha-7f3k"],["l4Oud…"]] 0:"$@1"
1:[{"id":1,"tea":"Mint tea","boxes":2},{"id":2,"tea":"Green tea","boxes":3}]

Line 1 is the key: the function's ID, the argument, and the build's ID. asha-7f3k is Asha's session, in plain text. Line 2 is the entry: her orders. Ben's session and orders came next. Here is what goes wrong:

  • Every server can read it. With a real remote store, like Redis, every server of the shop, and anyone who can get into the store, can read every visitor's session and orders. The docs say keys and tags are stored as written, not hashed.
  • It outlives the visit. The entry stays until it expires or the store removes it. When Asha signs out, her cookie goes; the entry with her session in it doesn't. The docs say Next.js never deletes from your store.
  • It's almost never reused. Each visitor makes a new key, so the store fills with entries only one person will ever read.
  • One mistake away from a leak. The key is the only thing keeping Asha's entry from Ben. Pass anything less unique than the session, and visitors share an entry.

The private version can't do any of this. The same printing handler is configured, and under $ next start it printed nothing: a private cache never reaches a cache handler. Its result lives for one request on the server, then only in the visitor's own browser.

Left, the mistake: one shared store holds two entries, asha-7f3k with Asha's orders and ben-2m9q with Ben's, keys in plain text, readable by every server. Right, a private cache: Asha's browser holds Asha's orders and Ben's browser holds Ben's, and the server keeps neither."use cache: remote"storeasha-7f3k: Mint, Greenben-2m9q: Lemon, Mintevery server reads it"use cache: private"AshaMint, GreenBenLemon, Minteach browser holds its own
Left: getOrders(session) with "use cache: remote". The store holds each visitor's session and orders, and every server can read it. Right: "use cache: private". Each visitor's orders reach only that visitor's browser.

The docs describe one more pattern: find the user's id from the session, and pass that stable id, never the session itself, to a plain "use cache" function that isn't exported, so no other code can ask for someone else's id. That keeps a copy on the server, keyed by the id, which some apps want. Choose "use cache: private" when the data must never be stored on the server, or when the cookie is read deep inside a helper you can't change. Authentication, later in the course, teaches sessions and that pattern.

'use cache: remote' and cacheHandlers

A remote cache earns its place with data that many visitors share, read at request time. The shop's menu shows prices in the visitor's currency, from a currency cookie. Only a few currencies exist, and the prices aren't personal:

lib/menu.ts: the menu, kept per currencyTypeScript
import { cacheLife } from "next/cache";

const api = process.env.SHOP_API_URL;

export type MenuItem = { name: string; price: string };

export async function getMenu(currency: string): Promise<MenuItem[]> {
  "use cache: remote";
  cacheLife("hours");
  const response = await fetch(`${api}/menu?currency=${currency}`);
  return response.json();
}

Line 8 marks getMenu with "use cache: remote". Line 9 gives it the hours lifetime, and line 10 asks the API for one currency. The page reads the cookie outside the cache and passes it in:

app/menu/page.tsx: the currency read outside the cacheTSX
import { Suspense } from "react";
import { cookies } from "next/headers";
import { getMenu } from "@/lib/menu";

export default function MenuPage() {
  return (
    <main>
      <h1>Menu</h1>
      <Suspense fallback={<p>Loading prices…</p>}>
        <Prices />
      </Suspense>
    </main>
  );
}

async function Prices() {
  const currency = (await cookies()).get("currency")?.value ?? "usd";
  const menu = await getMenu(currency);
  return (
    <ul>
      {menu.map((item) => (
        <li key={item.name}>
          {item.name}: {item.price}
        </li>
      ))}
    </ul>
  );
}

Line 17 reads the cookie, with "usd" when there's none, and line 18 passes it to getMenu. Lines 9 to 11 put Prices in Suspense, because it waits for the request. Three visitors came: Asha and Ben in dollars, Chen in euros. Pick a version:

Playground: 'use cache: remote' with and without a handler
The menu, without and with a remote handlerNext.js
app/menu/lib/
lib/menu.ts
import { cacheLife } from "next/cache"; const api = process.env.SHOP_API_URL; export type MenuItem = { name: string; price: string }; export async function getMenu(currency: string): Promise<MenuItem[]> {  "use cache: remote";  cacheLife("hours");  const response = await fetch(`${api}/menu?currency=${currency}`);  return response.json();}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (4/4)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ◐ /menu ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
Try it yourself
No cacheHandlers

lib/menu.ts marks getMenu with "use cache: remote" on line 8, and next.config.ts is the one create-next-app writes, with no cacheHandlers. Three visits, two currencies, and the API was asked twice: Ben's dollars came from Asha's copy. The copy lived in this server's memory, in the same store plain "use cache" uses.

Both versions were built with next build from Next.js 16.4.3 and visited by Asha and Ben in dollars, then Chen in euros, against the same stand-in API. Only next.config.ts and the handler file change. Reset goes back to the first version.

Both versions asked the API twice: once for dollars, once for euros. Ben's dollars came from Asha's copy. So what does a handler change? Where the copy lives.

Without a handler: the same memory as 'use cache'

"use cache: remote" only names a kind of store, called remote. If nothing provides one, Next.js uses its built-in store for both kinds. This is the code that sets them up when nothing is configured:

initializeCacheHandlers, from handlers.ts at v16.4.3 (the else branch)TypeScript
} else {
  const handler = createDefaultCacheHandler(builtInSize)
  builtInHandlers.add(handler)

  debug?.('setting "default" cache handler from default')
  handlersMap.set('default', handler)
  debug?.('setting "remote" cache handler from default')
  handlersMap.set('remote', handler)
}

Line 2 makes one in-memory store. Line 6 uses it for default, the kind plain "use cache" uses, and line 8 uses the same store for remote (handlers.ts, lines 106 to 114). So without a handler, a remote entry lives in one server's memory, like any other. A shop on three servers would ask the API for dollars up to three times. The docs add that hosting platforms usually provide a remote store for you; when you host the app yourself, you configure it.

A call to getMenu with usd, marked use cache: remote, goes to a question: is cacheHandlers.remote set in next.config.ts? If yes, an arrow to your handler, which talks to a store every server shares. If no, an arrow to each server's memory, the same store use cache uses.getMenu("usd")configremote handler?setsharedevery servernot setmemoryone per server
"use cache: remote" only names a kind of store. With cacheHandlers.remote set, your handler gets every get and set. Without it, Next.js 16.4.3 uses the in-memory store that "use cache" uses, one per server.

With a handler: your store

The second version sets a handler for remote in next.config.ts:

next.config.ts: a handler for 'use cache: remote'TypeScript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
  cacheHandlers: {
    remote: require.resolve("./cache-handlers/remote.js"),
  },
};

export default nextConfig;

Lines 6 to 8 are cacheHandlers. Its keys are kinds of store: remote here, default to replace the store of plain "use cache", or a name of your own, like sessions, which code then uses with "use cache: sessions". Each value is the path of a file that exports the handler. Don't mix it up with cacheHandler, singular: an older option for the store of the incremental cache, which keeps prerendered pages for Incremental Static Regeneration, Route Handler responses and optimized images. The docs say "use cache" never uses it. This is the tea shop's printing handler:

cache-handlers/remote.js: a handler that prints what it's givenJavaScript
// A stand-in for a store every server shares, like Redis. It keeps
// entries in a Map and prints what Next.js hands it.
const store = new Map();

function streamOf(bytes) {
  return new ReadableStream({
    start(controller) {
      controller.enqueue(bytes);
      controller.close();
    },
  });
}

module.exports = {
  async get(cacheKey) {
    const entry = store.get(cacheKey);
    console.log(entry ? "remote get, found:" : "remote get, missing:", cacheKey);
    return entry ? { ...entry, value: streamOf(entry.value) } : undefined;
  },
  async set(cacheKey, pendingEntry) {
    const entry = await pendingEntry;
    const value = new Uint8Array(await new Response(entry.value).arrayBuffer());
    store.set(cacheKey, { ...entry, value });
    console.log("remote set:", cacheKey, new TextDecoder().decode(value));
  },
  async refreshTags() {},
  async getExpiration() {
    return 0;
  },
  async updateTags() {},
};

Line 3 is the whole store: a Map in memory, standing in for Redis. get, on lines 15 to 19, looks the key up, prints whether it found it, and hands the stored bytes back as a new stream. set, on lines 20 to 25, waits for the entry on line 21, reads its value stream into bytes on line 22 (a stream can be read only once), keeps them on line 23, and prints them on line 24. Lines 26 to 30 are the tag methods, which this handler doesn't need. In the playground's Terminal, the handler printed remote get, missing and remote set for dollars, then remote get, found for Ben, then a miss and a set for euros. Each row is one method a handler has:

The five methods of a cache handler
MethodWhat it doesWhen Next.js calls it
get(cacheKey, softTags)Returns the stored entry for a key, or undefined when there's none.Each time a cached call runs, before the function body.
set(cacheKey, pendingEntry)Stores a new entry. Its value arrives as a stream, which can be read only once.After a miss, once the function has run.
refreshTags()Lets a handler learn which tags other servers expired.Once per request, before the first read from this handler.
getExpiration(tags)Says when any of these tags was last expired.When Next.js checks whether an entry is still good.
updateTags(tags, durations)Marks tags as expired.When code calls revalidateTag, updateTag or revalidatePath.

A real remote handler sends these to a shared store, and the docs point at a fuller Redis example. The key holds the build's ID (or a deployment ID, if you set one), so after a deploy the new build can't reach the old entries: remote entries don't survive a deploy, by design. The docs also give rules for nesting: a remote cache may call another remote cache or be called by a plain one, but a private cache and a remote cache can't call each other.

A shared store costs money and adds a network trip to every read. Each row is a question to ask before you reach for one:

When a remote cache pays off
QuestionRemote helps whenRemote doesn't help when
The data is read at request time, outside the static shell.Yes. Each server would otherwise fill its own copy.Not needed: a prerendered shell already holds it.
Few values go into the key, like a currency.Yes. Many visitors share each entry.No, if every visitor makes a new key: almost nothing is reused.
The data source is slow, expensive or rate-limited.Yes. One call fills the entry for every server.Less useful when the source is already fast.
The data changes every few seconds.Rarely. Entries go out of date before anyone reuses them.Read it fresh instead.

Where cached data lives

Put together, a cached result can be in five places. Each row is one place, what puts data there, and how long it may stay:

The places a cached result can be
PlaceWhat it holdsHow long
The build's outputResults cached while next build prerendered a route: they're part of that route's saved output, and a ◐ route's saved data can carry some to its requests.Until the next build.
Each server's memory"use cache", and "use cache: remote" without a handler of its own, at request time: one store in each server process, 50 MB by default (cacheMaxMemorySize).Until the process stops, the store is full, or the entry's lifetime ends.
A store your handler talks to"use cache: remote" with cacheHandlers.remote, or "use cache" with cacheHandlers.default: one store every server can share, like Redis.Until it expires, or your handler removes it. A new build's keys can't reach it.
One request"use cache: private" on the server, in production: calls with the same inputs share one result while the request lasts.Until the response is sent.
The visitor's browserWhat the router fetched for that visitor, private results included, kept in memory.Until its stale time ends, or the page reloads.

cacheLife sets the lifetimes, and each one says what may happen. stale is how long the browser may reuse a copy without asking. revalidate is when the server may serve the old copy and make a new one in the background. expire is when a request must wait for a new one. A store may still drop an entry sooner: in 16.4.3, the built-in in-memory store, in production, throws an entry away once it's past revalidate, so the next call misses and fills it again (default.ts, lines 114 to 116). That's how this version is built, not a promise of the API. A handler of your own decides for itself.

Two things don't move between places. The memory of the next build process isn't carried into next start; what the build cached lives on in the output it prerendered, which is how a shell can hold a cached result without asking again (The 'use cache' directive showed one). And nothing private ever reaches the server's stores at all.

Choosing a cache for each part of the shop

A real page mixes all of these. Ask two questions about each piece of data: whose is it, and how many different values go into its key? Sort the shop's data:

Which cache fits this data?
  • A visitor's order history, read with their session cookie.
  • Teas picked for the visitor by a session helper that reads cookies deep inside its own code.
  • Menu prices in the visitor's currency, read after a cookie, on a site that runs on many servers.
  • A rate-limited supplier's tea list, needed at request time by every server.
  • The tea of the day, the same for everyone, shown on a prerendered home page.
  • A tea's description, which changes once a month.
  • Live stock counts that must be fresh on every visit.
  • A check of whether the visitor's session token is still valid right now.
Try it yourself
0 of 8 correct

Sort each piece of the tea shop's data into the cache that fits it, or no cache.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A private cache stores each visitor's copy on the server." No. In production it keeps nothing on the server after the request. The three visits asked the API three times.
  • "'use cache: remote' shares entries between servers on its own." No. Without cacheHandlers.remote, or a store your hosting platform provides, it uses each server's own memory, like "use cache".
  • "Passing the session as an argument makes a shared cache safe." No. It keeps visitors' entries apart, but the session and the data still sit in plain text in a store every server reads.
  • "A private result is prerendered for each visitor." No. The build can't know who will visit. Private results are left out of the static shell and made at request time, or in a prefetch made for that visitor.
  • "A private cache may read anything about the request." No. cookies(), headers() and searchParams are allowed; connection() isn't.
  • "Remote entries survive a deploy." No. The key holds the build's ID, so a new build can't reach them.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upCount the API's answers

The orders page calls this getMyOrders from two components. Asha visits, Asha visits again, then Ben visits. How many times does the shop's API answer?

Starter codeTypeScript
import { cookies } from "next/headers";

const api = process.env.SHOP_API_URL;

export type Order = { id: number; tea: string; boxes: number };

export async function getMyOrders(): Promise<Order[]> {
  "use cache: private";
  const session = (await cookies()).get("session")?.value;
  const response = await fetch(`${api}/orders?session=${session}`);
  return response.json();
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the call that stops the build

    This file stops next build. Which call is the problem?

    Starter codeTypeScript
    import { cookies } from "next/headers";
    
    const api = process.env.SHOP_API_URL;
    
    export type Order = { id: number; tea: string; boxes: number };
    
    export async function getMyOrders(): Promise<Order[]> {
      "use cache: remote";
      const session = (await cookies()).get("session")?.value;
      const response = await fetch(`${api}/orders?session=${session}`);
      return response.json();
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead the shared store's key

      This is what the printing handler received when getOrders(session) ran for Asha. Which text in the key belongs to her?

      Starter codeText
      remote set: ["c0fb…",["asha-7f3k"],["l4Oud…"]] 0:"$@1"
      1:[{"id":1,"tea":"Mint tea","boxes":2},{"id":2,"tea":"Green tea","boxes":3}]

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeKeep the orders off the server

        This version puts every visitor's session and orders in the shared store. Which directive should replace line 6, so the orders are never stored on the server?

        Starter codeTypeScript
        const api = process.env.SHOP_API_URL;
        
        export type Order = { id: number; tea: string; boxes: number };
        
        export async function getOrders(session: string | undefined): Promise<Order[]> {
          "use cache: remote";
          const response = await fetch(`${api}/orders?session=${session}`);
          return response.json();
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeThree servers and no handler

          The shop runs this getMenu on three servers, with no cacheHandlers. Within an hour, each server gets a visitor who pays in dollars, and nothing else fills their memory. At most, how many times is the API asked for dollars?

          Starter codeTypeScript
          import { cacheLife } from "next/cache";
          
          const api = process.env.SHOP_API_URL;
          
          export type MenuItem = { name: string; price: string };
          
          export async function getMenu(currency: string): Promise<MenuItem[]> {
            "use cache: remote";
            cacheLife("hours");
            const response = await fetch(`${api}/menu?currency=${currency}`);
            return response.json();
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeChoose for a wishlist

            The shop adds a wishlist. A session helper the shop can't change reads the cookie inside its own code and returns the visitor's saved teas. The shop's rules say no visitor's wishlist may be stored on the server. Which directive fits?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask whose data it is, and where its copy is kept.

              The private and remote caches quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which of these can a function marked "use cache: private" read inside it?

                Choose an answer to see the explanation.

              2. Question 2 of 7Asha opens her orders page twice. How many times does the shop's API answer for her?

                Read the code, then predictTypeScript
                import { cookies } from "next/headers";
                
                const api = process.env.SHOP_API_URL;
                
                export type Order = { id: number; tea: string; boxes: number };
                
                export async function getMyOrders(): Promise<Order[]> {
                  "use cache: private";
                  const session = (await cookies()).get("session")?.value;
                  const response = await fetch(`${api}/orders?session=${session}`);
                  return response.json();
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 7This function runs for Asha and then for Ben. What does the remote store's key hold?

                Read the code, then predictTypeScript
                const api = process.env.SHOP_API_URL;
                
                export type Order = { id: number; tea: string; boxes: number };
                
                export async function getOrders(session: string | undefined): Promise<Order[]> {
                  "use cache: remote";
                  const response = await fetch(`${api}/orders?session=${session}`);
                  return response.json();
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7A site has no cacheHandlers and runs on three servers. Where do "use cache: remote" entries go?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which kinds of cache can cacheHandlers in next.config.ts give a store of their own?

                Choose an answer to see the explanation.

              6. Question 6 of 7After signing in, Asha clicks Your orders and the page shows her orders with no request at all. Why?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why is getMenu(currency) a better fit for "use cache: remote" than getOrders(session)?

                Read the code, then predictTypeScript
                import { cacheLife } from "next/cache";
                
                const api = process.env.SHOP_API_URL;
                
                export type MenuItem = { name: string; price: string };
                
                export async function getMenu(currency: string): Promise<MenuItem[]> {
                  "use cache: remote";
                  cacheLife("hours");
                  const response = await fetch(`${api}/menu?currency=${currency}`);
                  return response.json();
                }
                

                Choose an answer to see the explanation.

              Key takeaways

              • "use cache: private" may read cookies(), headers() and searchParams. In production it keeps its result for one request on the server, and the router may keep it in that visitor's browser.
              • A route that reads cookies gets a prefetch made for each visitor. With a private cache inside, the click can need no request at all.
              • Per-user data doesn't belong in a shared cache. Passing the session in keeps entries apart, but stores sessions and personal data in plain text, readable by every server, until they expire.
              • "use cache: remote" is for shared data read at request time, with few different keys. Without cacheHandlers.remote, it uses each server's memory, like "use cache".
              • cacheHandlers gives default, remote or a named kind a store of your own, through five methods: get, set, refreshTags, getExpiration and updateTags.
              • Remote entries don't survive a deploy, and a private cache can't be given a handler.

              Remember the one-liner.
              Shared data goes in a shared store; a visitor's own data stays with the visitor.

              Read it in Next.js's own code

              These links point at release v16.4.3, the version every example on this page was built with. handlers.ts sets up the stores, and default.ts is the built-in in-memory one. use-cache-wrapper.ts decides where each kind of cache reads and writes. The course's last stage, Under the hood, reads them.

              Coming next: Instant navigations and partial prefetching. It shows what a link prefetches, how to prefetch more or less, and how to guarantee a route stays static.

              CompleteFrontend Clear concepts. Working examples.