Next.js · 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
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.