Skip to main content
Next.js · lesson 5 of 11
Next.js intermediate Lesson 5 of 11

Caching with "use cache"

A Next.js 16 build fails fetching real data without "use cache"; the directive bakes one value into the build and reuses it until it expires.

By SkillByExample · Published

nextjs caching use-cache cache-components

Dynamic by Default: Rendering Modes showed Cache Components refusing to prerender a route that reads a request API — connection(), cookies(), headers() — without a fix. The same rule also catches a second, different category: uncached data, meaning a fetch(...) call or a database read. This lesson’s fix is the one the error itself recommends for that category, "use cache" — and this lesson runs that function’s data source as its own small Docker container, so the fetch is real, not a stand-in. The project setup reuses the Docker pattern from the first lesson — the node:26.13 image, the nextjs-modules volume, MSYS_NO_PATHCONV=1 on Git Bash — see it for why each flag is there.

Fetching Real Data Fails the Build, Too

Six files set up the project: package.json, next.config.ts, tsconfig.json, and app/layout.tsx are unchanged since Dynamic by Default: Rendering Modes; app/page.tsx carries a new comment, since this lesson’s new routes are the point, not the home page. data-server.js is new — a plain Node http server that stands in for an external API or a database: every request it gets, it answers with the current time as JSON.

{
  "name": "next-app",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "16.4.3",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "5.9.3",
    "@types/node": "20.19.43",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0"
  }
}
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
  "exclude": ["node_modules"]
}
// app/layout.tsx — required; every page in the app renders inside this
export const metadata = {
  title: "Next App",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
// app/page.tsx — a plain static home page; this lesson's new routes are /cached, /live, and /settings
export default function HomePage() {
  return <h1>Home</h1>;
}
// data-server.js — stands in for an external API or a database
const http = require("http");

http.createServer((req, res) => {
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({ now: Date.now() }));
}).listen(4000, () => console.log("listening on 4000"));

app/cached/page.tsx fetches from that stand-in data source and renders what it returns — no "use cache" yet, and nothing wraps the read in <Suspense> either:

// app/cached/page.tsx — fetches real data, no "use cache" yet
async function getTime() {
  const res = await fetch("http://nextjs-cache-data:4000/time");
  return res.json();
}

export default async function CachedPage() {
  const data = await getTime();
  return <p>Cached timestamp: {data.now}</p>;
}

The data source has to be reachable before anything can build or run against it, so every capture target in this lesson starts with the same two steps: create a Docker network, nextjs-cache-net, and start data-server.js on it as its own container, nextjs-cache-data. A direct curl confirms it on its own, before Next.js ever touches it. Predict whether next build catches a plain fetch(...) call the same way Dynamic by Default: Rendering Modes caught a bare connection() call, before building:

$ docker network create nextjs-cache-net
543b76f26db41b827f18d79881486dbf9105f0e06dffc1b76ece0acae8ad2d3a
$ docker run -d --name nextjs-cache-data --network nextjs-cache-net -p 4100:4000 -v "$(pwd)":/root/next-app -w /root/next-app node:26.13 node data-server.js
7ffc2b9b57f78b16362c50b88e845da8d5f67bba5e1771d6106da73410bdf2e5
$ until curl -s -o /dev/null http://localhost:4100/time; do sleep 0.3; done
$ curl -s http://localhost:4100/time
{"now":1793409838443}
$ docker run --rm -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npm install --no-audit --no-fund

added 28 packages in 25s
$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 57ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 4.6s
  Running TypeScript ...
  Finished TypeScript in 2.9s ...
Error: Route "/cached": Next.js encountered uncached or runtime data during prerendering.

`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route

Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
    at body (<anonymous>)
    at html (<anonymous>)
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 "/cached" 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 "/cached". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /cached/page: /cached, exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null

The build fails the same way /dynamic’s bare connection() call did — same error, same three fixes listed underneath it — except this time fetch(...) is the one actually responsible. Next.js never even has to reach data-server.js to know this: it bails out of rendering the route the moment it sees an un-cached fetch(...) call outside <Suspense>, before any real network request happens.

[cache]: Fix It With “use cache”

Add the directive inside the function that does the actual fetch — not the page component that calls it, and not at the top of the file:

// app/cached/page.tsx
async function getTime() {
  "use cache";
  const res = await fetch("http://nextjs-cache-data:4000/time");
  return res.json();
}

export default async function CachedPage() {
  const data = await getTime();
  return <p>Cached timestamp: {data.now}</p>;
}

Rebuild — data-server.js is already running on nextjs-cache-net from the previous step, so nothing else has to start first:

$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 79ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 3.3s
  Running TypeScript ...
  Finished TypeScript in 2.9s ...
  Finalizing page optimization ...

Route (app)      Revalidate  Expire
┌ ○ /
├ ○ /_not-found
└ ○ /cached             15m      1y


○  (Static)  prerendered as static content

The build succeeds, and /cached keeps the ○ marker — but the table itself grows two new columns, Revalidate and Expire, that no route in any earlier lesson has ever shown. They only appear now because app/cached/page.tsx actually has something to revalidate: a value "use cache" computed once, during this very build, by really calling getTime() and really reaching data-server.js over nextjs-cache-net. The two numbers are Next.js’s own default — this lesson never set either one — and cacheLife and cacheTag is what customizes them.

Same Value, Every Request — Even a Fresh Server

Start the app and request /cached twice, two seconds apart, exactly like Dynamic by Default: Rendering Modes did for connection() — except this time predict the opposite result, since the value was baked in at build time, not computed on each request:

$ docker run -d --name nextjs-cache-1 --network nextjs-cache-net -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
0ad4e046a5a382500c2d6ce0fc21f32a9c34aa2e3b33ce2d203cb3ccf660e75a
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ a=$(curl -s http://localhost:3100/cached | grep -oE '<p>Cached timestamp: [^/]+/p>'); echo "$a"
<p>Cached timestamp: <!-- -->1793409893651</p>
$ sleep 2; b=$(curl -s http://localhost:3100/cached | grep -oE '<p>Cached timestamp: [^/]+/p>'); echo "$b"
<p>Cached timestamp: <!-- -->1793409893651</p>
$ [ "$a" = "$b" ] && echo SAME
SAME
$ docker rm -f nextjs-cache-1
nextjs-cache-1

SAME only printed because the shell comparison found the two captured values equal. But that alone doesn’t prove the value is baked into the build rather than just held in this one server process’s memory — a brand-new container, started from the same files with no memory of the first one, settles that:

$ docker run -d --name nextjs-cache-2 --network nextjs-cache-net -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
a4d70436ac924b9f83166f91a21056c9194bcac75f8670c61024ac13a4ef97f5
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ c=$(curl -s http://localhost:3100/cached | grep -oE '<p>Cached timestamp: [^/]+/p>'); echo "$c"
<p>Cached timestamp: <!-- -->1793409893651</p>
$ [ "$a" = "$c" ] && echo SAME
SAME
$ docker rm -f nextjs-cache-2
nextjs-cache-2

$a still equals $c, even though nextjs-cache-2 never ran until after nextjs-cache-1 was already removed. The cached value lives in the build output on disk, not in any single running process — which is exactly why it survives a restart and will keep answering with this same value for up to 15 minutes, no matter how many times the server itself is stopped and started. That same build output persists further than a restart, too: Next.js caches the inner fetch() call itself by URL, in .next/cache/fetch-cache on the project directory, and every later next build in this lesson reuses that entry instead of calling getTime() on data-server.js again — which is why Practice 2’s $f, captured after two more rebuilds, still matches this exact $a.

[stream]: The Other Fix for Uncached Data Doesn’t Cache Anything

app/live/page.tsx fetches from the same data source, but fixes the build error the other valid way for uncached data — a <Suspense> boundary, with no "use cache" at all:

// app/live/page.tsx — same fetch, wrapped in Suspense instead of "use cache"
import { Suspense } from "react";

async function getTime() {
  const res = await fetch("http://nextjs-cache-data:4000/time");
  return res.json();
}

async function Timestamp() {
  const data = await getTime();
  return <p>Live timestamp: {data.now}</p>;
}

export default function LivePage() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Timestamp />
    </Suspense>
  );
}

Predict whether /live gets ○, like /cached, or the ◐ marker Dynamic by Default: Rendering Modes gave a <Suspense>-wrapped connection() call, before building:

$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 56ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 3.6s
  Running TypeScript ...
  Finished TypeScript in 2.4s ...
  Finalizing page optimization ...

Route (app)      Revalidate  Expire
┌ ○ /
├ ○ /_not-found
├ ○ /cached             15m      1y
└ ◐ /live


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

/live gets ◐, with no Revalidate/Expire columns of its own — there is nothing to revalidate, because <Suspense> never asked Next.js to cache anything; it only agreed to let the route ship without that one piece of data ready yet. Two requests, two seconds apart, confirm the behavioral gap directly:

$ docker run -d --name nextjs-cache-3 --network nextjs-cache-net -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
0b5ba6f7c158030acc8b279222b336dad28f4adde9331d290c916f14444e12e4
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ d=$(curl -s http://localhost:3100/live | grep -oE '<p>Live timestamp: [^/]+/p>'); echo "$d"
<p>Live timestamp: <!-- -->1793409923700</p>
$ sleep 2; e=$(curl -s http://localhost:3100/live | grep -oE '<p>Live timestamp: [^/]+/p>'); echo "$e"
<p>Live timestamp: <!-- -->1793409926067</p>
$ [ "$d" != "$e" ] && echo DIFFERENT
DIFFERENT
$ docker rm -f nextjs-cache-3
nextjs-cache-3

DIFFERENT only printed because $d and $e really were unequal — /live recomputes its fetch on every request, exactly like /cached did before "use cache" was added, except this version never fails the build. <Suspense> fixes uncached data the same way it fixed a request API in Dynamic by Default: Rendering Modes: by letting the route ship without that value ready, not by remembering it for next time.

A Real Failure: Request Data Inside “use cache”

app/settings/page.tsx tries the opposite combination on purpose — reading a request API, cookies(), from inside a function already marked "use cache":

// app/settings/page.tsx — reads a request API inside a "use cache" function, on purpose
import { cookies } from "next/headers";

async function getTheme() {
  "use cache";
  const cookieStore = await cookies();
  return cookieStore.get("theme")?.value ?? "none";
}

export default async function SettingsPage() {
  const theme = await getTheme();
  return <p>Theme: {theme}</p>;
}

Predict whether the build fails with the same generic error /cached hit at the very start of this lesson, or something more specific to this particular combination, before building:

$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 66ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 3.7s
  Running TypeScript ...
  Finished TypeScript in 3.2s ...
⨯ Error: Route "/settings": `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 (app/settings/page.tsx:6:29)
  4 | async function getTheme() {
  5 |   "use cache";
> 6 |   const cookieStore = await cookies();
    |                             ^
  7 |   return cookieStore.get("theme")?.value ?? "none";
  8 | }
  9 | {
  digest: '1750071219'
}
Error: Route "/settings": `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 (app/settings/page.tsx:6:29)
  4 | async function getTheme() {
  5 |   "use cache";
> 6 |   const cookieStore = await cookies();
    |                             ^
  7 |   return cookieStore.get("theme")?.value ?? "none";
  8 | }
  9 | {
  digest: '1750071219'
}
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 "/settings" 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 "/settings". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /settings/page: /settings, exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null

This is a different, more specific error — not the generic “uncached or runtime data” message /cached triggered earlier, but one named exactly for this combination: cookies() can’t be read inside "use cache". A cached function’s whole point is to compute one value and hand the same answer to many different requests; cookies() returns something that’s different for every request by definition, so Next.js refuses outright rather than let one visitor’s cookie leak into the cached response every other visitor gets.

Practice

Each task below builds on the files the previous ones (and the main lesson above) left in place — do them in order. The last task also removes the data source and network this lesson created.

1. The error above suggests moving cookies() outside the cached function. Try exactly that first, with no <Suspense> yet, and predict whether the build error disappears or just changes shape. Then add a <Suspense> boundary around the new outer read and confirm the build finally succeeds.

Solution
// app/settings/page.tsx — cookies() moved outside the cached function, no <Suspense> yet
import { cookies } from "next/headers";

async function getTheme(theme: string) {
  "use cache";
  return theme.toUpperCase();
}

export default async function SettingsPage() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "none";
  const result = await getTheme(theme);
  return <p>Theme: {result}</p>;
}
$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 97ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 3.0s
  Running TypeScript ...
  Finished TypeScript in 2.2s ...
Error: Route "/settings": Next.js encountered uncached or runtime data during prerendering.

`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route

Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
    at body (<anonymous>)
    at html (<anonymous>)
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 "/settings" 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 "/settings". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /settings/page: /settings, exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null

Still fails — but now with the same generic “uncached or runtime data” error /cached hit at the start of this lesson, naming /settings instead. Moving cookies() out of the cached function stopped the specific cache violation, but a bare cookies() read outside <Suspense> still trips the ordinary rule from Dynamic by Default: Rendering Modes. Wrap that outer read in <Suspense> too:

// app/settings/page.tsx — cookies() outside the cached function, wrapped in Suspense
import { Suspense } from "react";
import { cookies } from "next/headers";

async function getTheme(theme: string) {
  "use cache";
  return theme.toUpperCase();
}

async function Theme() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "none";
  const result = await getTheme(theme);
  return <p>Theme: {result}</p>;
}

export default function SettingsPage() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Theme />
    </Suspense>
  );
}
$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 76ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 2.7s
  Running TypeScript ...
  Finished TypeScript in 2.2s ...
  Finalizing page optimization ...

Route (app)      Revalidate  Expire
┌ ○ /
├ ○ /_not-found
├ ○ /cached             15m      1y
├ ◐ /live
└ ◐ /settings


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

The build finally succeeds, and /settings joins /live with the ◐ marker. Both changes were required — moving the read out of the cached function, and giving that read its own <Suspense> boundary — neither one alone was enough.

2. Stop nextjs-cache-data while a server built from this lesson’s files is running, and confirm which route still answers and which one doesn’t.

Solution
$ docker run -d --name nextjs-cache-4 --network nextjs-cache-net -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
6c02ad6dcb8651ae9eff7a9bc2f751056e6e6597b44f6da158e56efb55ef232b
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ f=$(curl -s http://localhost:3100/cached | grep -oE '<p>Cached timestamp: [^/]+/p>'); echo "$f"
<p>Cached timestamp: <!-- -->1793409893651</p>
$ [ "$a" = "$f" ] && echo SAME
SAME
$ docker rm -f nextjs-cache-data
nextjs-cache-data
$ g=$(curl -s http://localhost:3100/cached | grep -oE '<p>Cached timestamp: [^/]+/p>'); echo "$g"
<p>Cached timestamp: <!-- -->1793409893651</p>
$ [ "$f" = "$g" ] && echo SAME
SAME
$ curl -s http://localhost:3100/live | grep -c 'Loading\.\.\.'
1
$ docker run -d --name nextjs-cache-data --network nextjs-cache-net -p 4100:4000 -v "$(pwd)":/root/next-app -w /root/next-app node:26.13 node data-server.js
9a19778c50092b67bde7c5680770cdde0d7a89c1d867297519689a0122694ec4
$ until curl -s -o /dev/null http://localhost:4100/time; do sleep 0.3; done
$ docker rm -f nextjs-cache-4
nextjs-cache-4

The first SAME confirms $f still matches $a, captured all the way back in the section above, after several rebuilds in between — the fetch-cache reuse this lesson already explained. The second SAME confirms /cached answers with $f both before and after nextjs-cache-data is removed, even though the data source it originally fetched from no longer exists. /live isn’t so lucky: its response shows the literal fallback text, Loading..., exactly once, with no timestamp ever arriving — the fetch it depends on now has nowhere to go, and nothing caught the failure. The last two steps restart the data source so the remaining practice tasks have it available again.

3. Call the same cached function twice with two different arguments — getGreeting("Ada") and getGreeting("Grace") — and confirm each gets its own cached value, while each one still stays stable on a second request.

Solution
// app/greet/page.tsx — two calls to the same cached function with different arguments
async function getGreeting(name: string) {
  "use cache";
  return `Hello, ${name}, at ${Date.now()}`;
}

export default async function GreetPage() {
  const ada = await getGreeting("Ada");
  const grace = await getGreeting("Grace");
  return (
    <>
      <p>Ada: {ada}</p>
      <p>Grace: {grace}</p>
    </>
  );
}
$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 61ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 3.8s
  Running TypeScript ...
  Finished TypeScript in 3.0s ...
  Finalizing page optimization ...

Route (app)      Revalidate  Expire
┌ ○ /
├ ○ /_not-found
├ ○ /cached             15m      1y
├ ○ /greet              15m      1y
├ ◐ /live
└ ◐ /settings


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
$ docker run -d --name nextjs-cache-5 --network nextjs-cache-net -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
65150ee703c7ee9aaa0f821e8b7386835f2ec22e66217630c3fcb73181b7ba3e
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ h=$(curl -s http://localhost:3100/greet | grep -oE '<p>Ada: [^/]+/p>'); echo "$h"
<p>Ada: <!-- -->Hello, Ada, at 1793409994238</p>
$ i=$(curl -s http://localhost:3100/greet | grep -oE '<p>Grace: [^/]+/p>'); echo "$i"
<p>Grace: <!-- -->Hello, Grace, at 1793409994244</p>
$ [ "$h" != "$i" ] && echo DIFFERENT
DIFFERENT
$ j=$(curl -s http://localhost:3100/greet | grep -oE '<p>Ada: [^/]+/p>'); [ "$h" = "$j" ] && echo SAME
SAME
$ docker rm -f nextjs-cache-5
nextjs-cache-5

Ada and Grace print DIFFERENT greetings — "use cache" keys its cache by the arguments a function is called with, so two different names get two separate cached entries, each computed during the same build. Calling getGreeting("Ada") again on a later request still returns $h’s exact value, SAME as the first call — each entry is just as stable as /cached’s was.

4. Add export const instant = false to a page that fetches from the data source instead of wrapping it in <Suspense>, and confirm it behaves like /live, not like /cached.

Solution
// app/block/page.tsx — same fetch as /live, instant = false instead of Suspense
export const instant = false;

async function getTime() {
  const res = await fetch("http://nextjs-cache-data:4000/time");
  return res.json();
}

export default async function BlockPage() {
  const data = await getTime();
  return <p>Blocking timestamp: {data.now}</p>;
}
$ docker run --rm --network nextjs-cache-net -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 -e NPM_CONFIG_UPDATE_NOTIFIER=false node:26.13 npx next build 2>&1 | grep -v -E "Generating static pages using|Collecting page data using|Finished filesystem cache database compaction"
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 72ms
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...
✓ Compiled successfully in 3.7s
  Running TypeScript ...
  Finished TypeScript in 2.9s ...
  Finalizing page optimization ...

Route (app)      Revalidate  Expire
┌ ○ /
├ ○ /_not-found
├ ƒ /block
├ ○ /cached             15m      1y
├ ○ /greet              15m      1y
├ ◐ /live
└ ◐ /settings


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
ƒ  (Dynamic)            server-rendered on demand
$ docker run -d --name nextjs-cache-6 --network nextjs-cache-net -p 3100:3000 -v "$(pwd)":/root/next-app -v nextjs-modules:/root/next-app/node_modules -w /root/next-app -e NEXT_TELEMETRY_DISABLED=1 node:26.13 npx next start
f947d973a1bd64e60a6b836b759bdb9d26b130c816317e9604c1a1a3415ab884
$ until curl -s -o /dev/null http://localhost:3100/; do sleep 0.3; done
$ k=$(curl -s http://localhost:3100/block | grep -oE '<p>Blocking timestamp: [^/]+/p>'); echo "$k"
<p>Blocking timestamp: <!-- -->1793410022062</p>
$ sleep 2; l=$(curl -s http://localhost:3100/block | grep -oE '<p>Blocking timestamp: [^/]+/p>'); echo "$l"
<p>Blocking timestamp: <!-- -->1793410024933</p>
$ [ "$k" != "$l" ] && echo DIFFERENT
DIFFERENT
$ docker rm -f nextjs-cache-6
nextjs-cache-6
$ docker rm -f nextjs-cache-data
nextjs-cache-data
$ docker network rm nextjs-cache-net
nextjs-cache-net

/block gets ƒ, the marker Dynamic by Default: Rendering Modes gave connection() with instant = false, not the ◐ /live earned — there is no static shell at all. Its two requests still print DIFFERENT, exactly like /live’s did: instant = false fixes the build the same way <Suspense> does, by letting the route skip prerendering, not by caching anything. Of this lesson’s three fixes, "use cache" is the only one that ever makes two requests agree. The last two steps remove the data source and the network this lesson created.

Frequently Asked Questions

What's the real difference between the three fixes Cache Components' build error lists?
[stream] (<Suspense>) and [block] (export const instant = false) both work on any triggering read — a request API like cookies() or uncached data like fetch() — because neither one caches anything; they only decide whether the route still gets a static shell with a streamed-in hole (◐) or no shell at all (ƒ). [cache] ("use cache") only applies to uncached data — a fetch(...) call or a database read — never to a request API, which [Dynamic by Default: Rendering Modes](/nextjs/rendering-modes/) already showed it explicitly excludes. Wrapping a fetch in "use cache" instead bakes its result into the build and reuses that exact value on every later request, until the cache entry expires.
Does the data a "use cache" function fetches have to be reachable while next build runs?
Only for a first, clean build. A "use cache"-marked function runs during prerendering to compute the value it bakes into the static output, so the data source has to be reachable the first time that build happens. Next.js also caches the inner fetch() call itself by URL, in .next/cache/fetch-cache, which sits on the project directory and survives between separate next build runs — so a later rebuild that fetches the same URL reuses that cached response instead of making a new request, even if the data source has since gone down. A plain, uncached fetch() never gets this far at all: Next.js bails out of rendering the route the instant it sees the call, before any real network request happens, which is why the same fetch() fails the build identically whether or not its target is actually running.
Why did reading cookies() inside a "use cache" function produce a different error than reading it directly in a page?
A bare cookies() call outside <Suspense> hits the generic "encountered uncached or runtime data during prerendering" rule — the same one a bare fetch() or connection() call hits, covered in [Dynamic by Default: Rendering Modes](/nextjs/rendering-modes/). cookies() called from inside a function already marked "use cache" hits a second, stricter, more specific check instead: Next.js refuses outright, because a cached function's whole point is to compute one value and reuse it across many different requests, and cookies() returns something that's different per request by definition.
Are the Revalidate and Expire columns in the build's route table something this lesson sets directly?
No. 15m and 1y are the default cache-life profile Next.js applies automatically to any function marked "use cache" that doesn't request a different one. [cacheLife and cacheTag](/nextjs/cachelife-and-cachetag/) customizes exactly these two numbers; this lesson only ever sees the default.
If the data source a cached function reads from goes down later, does the cached route break too?
No. Once a value is baked into the build, the cached route never touches its data source again until the cache entry expires, so it keeps serving that same response even while the source is completely unreachable. An uncached route reading the same source has no such protection — it depends on that source answering on every single request.