Topic 20.5
Data and Caching in Next.js
In one line
In Next.js 16 server components await their data directly; with Cache Components enabled you cache functions or components with "use cache", set lifetimes with cacheLife, tag them with cacheTag and refresh them with revalidateTag, updateTag or revalidatePath, while request-time data streams behind <Suspense>, so one route can be part static, part dynamic.
Think of it like this
A school canteen. Some food is cooked in the morning and kept warm all day (static, cached at build time). Some dishes are cooked again every hour (time-based revalidation). When the cook changes the menu, the old dish is thrown away at once (on-demand revalidation). And your own order with extra cheese is made only when you ask (dynamic, per request). A good canteen serves the ready food immediately and brings your special order a moment later (streaming).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Static rendering
- Rendering at build time (or in the background) and serving the saved result to everyone.
- Dynamic rendering
- Rendering on every request because the result depends on that request.
- Runtime API
- Something only known when a request arrives:
cookies(),headers(),searchParams, unknownparams. - Cache Components
- The Next.js 16 mode (
cacheComponents: true) where you mark cached work with"use cache"and stream the rest. - "use cache"
- A directive that caches a function's or component's result, keyed by its arguments.
- cacheLife
- Sets how long a cached entry stays fresh and when it is refreshed or thrown away.
- cacheTag / revalidateTag / updateTag
- Label cached entries, then mark them stale (refresh in background) or expire them right away.
- Static shell
- The prerendered part of a page, served instantly, with fallbacks where dynamic parts will stream in.
- Memoization
- Remembering a result so the same call in the same render doesn't run twice.
Step by step
01Fetch in an async server component
The page awaits its data. Start independent requests together so they don't form a waterfall.
import { getArtist, getAlbums } from "@/lib/data";
export default async function Page({ params }: PageProps<"/artist/[username]">) {
const { username } = await params;
// start both, then wait for both
const [artist, albums] = await Promise.all([getArtist(username), getAlbums(username)]);
return (
<>
<h1>{artist.name}</h1>
<ul>{albums.map((a) => <li key={a.id}>{a.title}</li>)}</ul>
</>
);
}02Turn on Cache Components
One flag in the config switches the app to the Next.js 16 caching model with partial prerendering.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;03Cache data with "use cache", cacheLife and cacheTag
The directive goes at the top of the function body. The tag lets a Server Action refresh it after an edit.
import { cacheLife, cacheTag } from "next/cache";
import { db } from "@/lib/db";
export async function getProducts() {
"use cache";
cacheLife("hours"); // stale 5 min, revalidate 1 h, expire 1 day
cacheTag("products");
return db.product.findMany();
}04Stream what depends on the request
Code that reads cookies can't be in the static shell. Wrap it in Suspense: its fallback goes in the shell and the real content streams per request. Everything else on the page stays static.
import { Suspense } from "react";
import { cookies } from "next/headers";
import { getPosts } from "@/lib/data"; // "use cache" inside
export default function BlogPage() {
return (
<>
<h1>Our Blog</h1> {/* static */}
<BlogPosts /> {/* cached, in the shell */}
<Suspense fallback={<p>Loading your preferences...</p>}>
<UserPreferences /> {/* streams per request */}
</Suspense>
</>
);
}
async function BlogPosts() {
const posts = await getPosts();
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
async function UserPreferences() {
const theme = (await cookies()).get("theme")?.value ?? "light";
return <p>Your theme: {theme}</p>;
}05Refresh after a change
After a mutation, expire what changed. updateTag makes the author see their own edit immediately; revalidateTag(tag, "max") is for background refreshes such as a CMS webhook.
"use server";
import { updateTag } from "next/cache";
import { redirect } from "next/navigation";
export async function createProduct(formData: FormData) {
// ...authenticate, authorise, validate (Topic 20.3)...
const product = await db.product.create({ data: { name: String(formData.get("name")) } });
updateTag("products"); // the list is fresh on the next render
redirect(`/products/${product.id}`); // call after updating the cache
}Try it yourself
- 1
Different arguments, different entries
In the cache model, call
getPrice("book")after the first read. PredictdbReads. (It goes up: the argument is part of the cache key.) - 2
Swap revalidateTag for updateTag
Replace the
revalidateTagcall withupdateTag. Which line changes, and why would an author editing a product prefer this? - 3
Make the shell wait
In the streaming model, remove the
<Suspense>aroundUserPreferences. Predict how many chunks arrive. That is what Next.js warns about as a blocking route.
Code & diagrams
A plain-JavaScript model of the behaviour described in the Next.js 16 docs, not Next.js itself. The second read hits the cache; after `revalidateTag` one visitor still sees ₹100 while ₹120 loads; after `updateTag` the very next read waits and gets ₹150.
Preview
Console
Nothing logged yet.Expected console output
pen: ₹100 | db reads: 1
pen: ₹100 | db reads: 1
pen: ₹100 (stale, refreshing)
pen: ₹120 | db reads: 2
pen: ₹150 (read-your-own-writes) | db reads: 3The same rule as Topic 14.1, now inside an async server component: each `await` pauses the function, so independent requests should start before you wait.
Preview
Console
Nothing logged yet.Expected console output
Sequential (a waterfall):
start artist
end artist
start albums
end albums
Parallel (start both, then await):
start artist
start albums
end artist
end albums
A. R. Rahman [ 'Roja', 'Lagaan' ]A model of partial prerendering with real `react-dom/server` streaming: in Next.js the first chunk would be saved at build time and served from a CDN, and only the `UserPreferences` part would be rendered per request.
Preview
Console
Nothing logged yet.Expected console output
static shell: Our Blog | Server Components | Caching in Next.js | Loading your preferences...
streamed hole: Our Blog | Server Components | Caching in Next.js | Your theme: dark// app/blog/[id]/page.tsx: ISR in the previous model
export const revalidate = 60; // rebuild at most once a minute
export async function generateStaticParams() {
const posts = await fetch("https://api.example.com/posts", { cache: "force-cache" }).then((r) => r.json());
return posts.map((p: { id: number }) => ({ id: String(p.id) }));
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await fetch(`https://api.example.com/posts/${id}`, {
next: { revalidate: 60, tags: ["posts"] }, // cached, refreshed, taggable
}).then((r) => r.json());
return <h1>{post.title}</h1>;
}
// elsewhere: export const dynamic = "force-dynamic"; // always render per requestBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Read cookies inside "use cache"
In a cached function write "use cache"; const theme = (await cookies()).get("theme");.
Break #2
Expect edits to show up in development caching tests
Test cacheLife("hours") with npm run dev and wonder why every refresh shows new data.
Myth vs fact
Myth
fetch in Next.js is cached by default.
Fact
That was Next.js 13 and 14. Since Next.js 15 fetch is not cached unless you opt in ("use cache", or cache: "force-cache" in the previous model).
Myth
Reading cookies makes the whole page dynamic.
Fact
With Cache Components only the part inside the nearest <Suspense> is rendered per request; the rest stays in the static shell. (In the previous model, cookies() did make the whole route dynamic.)
Myth
revalidatePath and revalidateTag rebuild the page immediately.
Fact
They mark data stale (revalidateTag with stale-while-revalidate). The new version is generated on a following request. Use updateTag in a Server Action when the user must see their own change at once.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Every cache in Next.js is scoped to a deployment: the build id is part of the key, so a new deploy starts with fresh prerenders and empty
"use cache"entries. On serverless hosts the default in-memory store is per instance and short-lived;"use cache: remote"moves entries to a shared cache handler, worth it only with a high hit rate. - ▸
"Short-lived" caches (the
secondsprofile,revalidate: 0, orexpireunder 5 minutes) are excluded from prerendering and become dynamic holes automatically. - ▸
Push dynamic access down the tree: pass
paramsorsearchParamspromises to a child inside Suspense instead of awaiting them in a layout, so the layout and everything else stays in the static shell. - ▸
Bots get the whole page rendered at request time (no shell), so data used in the shell must also be reachable at request time, not only during the build.
Remember this
- 1
Fetching in server components: make the component
asyncandawaityour data withfetchor a database client:const posts = await db.post.findMany(). NouseEffect, no loading state in the component itself, and no API route needed. Identicalfetchcalls in one render are memoized (deduplicated), and for database calls you wrap the function in React's **cache** so several components can callgetUser(id)and share one result per request. Watch for waterfalls (Topic 14.1): twoawaits in a row run one after the other; start independent requests together withPromise.all, or let separate components fetch in parallel inside their own Suspense boundaries. - 2
Static vs dynamic rendering. Static (prerendered) means Next.js renders at build time (or in the background later) and serves the saved result to everyone. Dynamic means it renders on each request because the output depends on that request. Runtime APIs make a part dynamic:
cookies(),headers(),searchParams, andparamsthat weren't listed ingenerateStaticParams. Uncached data (afetchor query without a cache directive) and values that change every call (Date.now(),Math.random(),crypto.randomUUID()) are also request-time work. Since Next.js 15,fetchis not cached by default. - 3
Cache Components (Next.js 16). Turn it on with
cacheComponents: trueinnext.config.ts. Then every route is partially prerendered: at build time Next.js renders a static shell (everything that can finish without a request, plus Suspense fallbacks), and the dynamic parts stream in at request time (Topic 20.1). Next.js asks you to be explicit: request-time work must either be cached with"use cache"or **wrapped in<Suspense>**; otherwise the dev overlay and the build report a "blocking route" and suggest the fix. For a value that must be unique per request, callawait connection()(fromnext/server) first and wrap the component in Suspense. - 4
**
"use cache"** caches the return value of an async function (data level:getProducts()) or a whole component or page (UI level), and"use cache"at the top of a file caches all its exports. The function's arguments and captured variables become the cache key, sogetUser("1")andgetUser("2")are cached separately, and arguments must be serializable. You can't readcookies()orheaders()inside a"use cache"scope: read them outside and pass the value in as an argument (or use the"use cache: private"variant). Cached output becomes part of the static shell when its lifetime is long enough. - 5
Lifetimes and revalidation.
cacheLife("hours")(fromnext/cache) sets how long an entry is used: built-in profiles aredefault,seconds,minutes,hours,days,weeksandmax, each withstale,revalidateandexpiretimes (forhours: 5 minutes, 1 hour, 1 day), or pass your own{ stale, revalidate, expire }. Time-based revalidation refreshes an entry afterrevalidate. On-demand revalidation after a change:cacheTag("posts")labels an entry;revalidateTag("posts", "max")marks it stale (the next visitor gets the old data while fresh data loads, stale-while-revalidate), usable in Server Actions and Route Handlers;updateTag("posts")expires it immediately so the user who just made a change sees it (read-your-own-writes), only in Server Actions;revalidatePath("/blog")invalidates everything for a path;refresh()re-renders the current page without touching cached data. - 6
Without Cache Components (the previous model, still common in existing apps): cache a request with
fetch(url, { cache: "force-cache" }), revalidate it with{ next: { revalidate: 3600 } }or tag it with{ next: { tags: ["posts"] } }; set a whole page's default withexport const revalidate = 60(this is ISR together withgenerateStaticParams) or force request-time rendering withexport const dynamic = "force-dynamic";unstable_cachecaches non-fetch functions. The build output shows the result per route:○static,●SSG,◐partial prerender (Cache Components),ƒdynamic. In development, pages are always rendered on demand, so check caching withnext buildandnext start.
Explain it without notes
How do you fetch data in a Next.js server component, and how do you avoid waterfalls?
What makes part of a route dynamic, and how does Cache Components handle it?
Explain "use cache", cacheLife and cacheTag. What is part of the cache key?
Compare revalidateTag, updateTag and revalidatePath.
Practice
Write a cached getCategory(slug) that lasts a day and can be refreshed by the tag categories.
A product page shows product details (same for everyone) and "Recently viewed" (from a cookie). Write the page so the details are in the static shell.
Trade-offs
- ↔
Longer cache lifetimes mean faster, cheaper pages but staler data; tags with on-demand revalidation give both, at the cost of remembering to invalidate after every mutation.
- ↔
Caching UI (a whole component) saves the most work but is the coarsest; caching data functions is more reusable across pages.
Done when you can
I can fetch data in async server components without waterfalls.
I can tell which code makes a route dynamic and wrap it in Suspense.
I can use
"use cache",cacheLifeandcacheTag.I can choose between
revalidateTag,updateTag,revalidatePathandrefresh.I can read the build route table symbols.
I know the previous model's
fetchoptions andrevalidate/dynamicexports.