Next.js Caching Layers
- Request Memoization — deduplicates identical
fetch()calls in one render tree - Data Cache — persists fetch results across requests and deployments
- Full Route Cache — caches rendered HTML & RSC payloads at build time
- Router Cache — client-side cache of visited route segments
Revalidation Options
- Time-based:
next: { revalidate: 60 } - On-demand:
revalidatePath()andrevalidateTag()in Server Actions
Caching & Revalidation Strategies
Caching is what separates a fast app from a slow one. Done right, your pages load instantly because the server already has the answer ready. Done wrong, users wait for the same data to be fetched over and over, or worse — they see stale data that never updates.
Next.js has one of the most sophisticated caching systems of any web framework — four distinct layers that each serve a different purpose. This lesson breaks all four down, shows you exactly when each one activates, and teaches you how to invalidate cached data on demand so your app always shows the right content at the right time.
1. The Four Caching Layers
Next.js caches at four different levels. Understanding each one individually prevents a huge amount of confusion about why data is or isn't updating:
| Layer | What it caches | Where it lives | Duration |
|---|---|---|---|
| Request Memoization | Identical fetch() results within one render |
Server memory | Single request lifetime |
| Data Cache | fetch() responses |
Server filesystem / persistent store | Until revalidated or redeployed |
| Full Route Cache | Rendered HTML + RSC payload | Server filesystem | Until revalidated or redeployed |
| Router Cache | RSC payloads of visited routes | Browser memory | Session (30s for dynamic, 5min for static) |
Let's go through each one in depth so you know exactly what's happening and when to opt out.
2. Layer 1 — Request Memoization
You already saw this in Lesson 202. During a single server render, Next.js
deduplicates identical fetch() calls — if your layout and your
page both fetch the same URL, only one actual network request is made.
// Both of these run during the same render:
// app/layout.tsx
const user = await fetch("/api/me"); // ← Network request made here
// app/dashboard/page.tsx
const user = await fetch("/api/me"); // ← Returns memoized result, no network call
Key things to know about request memoization:
- It only lasts for the duration of one server request — it's not persistent.
- It only deduplicates GET requests — POST, PUT, DELETE are never memoized.
- It only works for the native
fetch()function. For database queries, use React'scache()wrapper. - You can't opt out of it — it's always on and has no downside.
3. Layer 2 — Data Cache
The Data Cache is Next.js's persistent server-side cache for fetch()
responses. Unlike memoization which resets per request, the Data Cache persists
across requests and even across deployments (on Vercel).
This is the cache you control with the cache and next
options on fetch():
Opting Out — no-store
// Never cache — always go to the origin
const res = await fetch("https://api.example.com/live-scores", {
cache: "no-store",
});
Use no-store for data that must always be fresh: live sports scores,
real-time prices, anything where a cached response would be wrong.
Caching Forever — force-cache
// Cache indefinitely — never re-fetch until redeployed or manually revalidated
const res = await fetch("https://api.example.com/countries", {
cache: "force-cache", // This is the default for fetch() in Next.js
});
Use force-cache for truly static data — a list of countries,
supported currencies, or configuration from a CMS you control.
Time-based Revalidation
// Cache for 1 hour, then fetch fresh on the next request after expiry
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 3600 },
});
The revalidate number is in seconds. After the window expires,
the next incoming request triggers a background re-fetch. The stale data is
served to that first request while the fresh data is being fetched —
this is called stale-while-revalidate. Subsequent requests
get the fresh data.
Tag-based Revalidation
// Tag this fetch so it can be invalidated on demand
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
Tags let you attach a label to a cached response. You can then call
revalidateTag("posts") from a Server Action or Route Handler to
instantly invalidate all cached responses with that tag — regardless of when
they were cached. We'll see this in action in the revalidation section below.
4. Layer 3 — Full Route Cache
The Full Route Cache caches the rendered output of entire routes — the HTML and the RSC (React Server Component) payload — at build time or after the first request.
When Next.js builds your app (npm run build), it pre-renders every
route it can — any route that doesn't use dynamic functions or no-store
fetches gets rendered to static HTML and stored. When a user requests that route,
the server serves the pre-rendered HTML instantly — no rendering, no data fetching,
just serving a file. This is as fast as web serving gets.
Static vs Dynamic Routes
A route is statically rendered (Full Route Cache active) when it:
- Doesn't use
cookies(),headers(), orsearchParams - Doesn't use
fetchwithcache: "no-store" - Doesn't use dynamic segment params that aren't pre-generated with
generateStaticParams
A route is dynamically rendered (Full Route Cache bypassed) when it:
- Uses
cookies()orheaders()— because these are per-request - Uses
searchParams— because the URL can be anything - Uses
fetchwithcache: "no-store"
generateStaticParams — Pre-rendering Dynamic Routes
For dynamic routes like /blog/[slug], you can tell Next.js which slugs
to pre-render at build time using generateStaticParams:
// app/blog/[slug]/page.tsx
// Tell Next.js which slugs to pre-render at build time
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
return <article>{post?.title}</article>;
}
At build time, Next.js calls generateStaticParams, gets all the slugs,
and pre-renders a static HTML file for each one. Requests to those blog posts
are served from the Full Route Cache instantly — no server-side rendering needed
at request time.
5. Layer 4 — Router Cache
The Router Cache is a client-side cache that lives in the browser. When a user navigates to a route, Next.js stores the RSC payload in memory. If they navigate away and come back within the cache window, the page is served from this in-memory cache — instantly, with no server request at all.
Cache durations:
- Static routes: cached for 5 minutes
- Dynamic routes: cached for 30 seconds
This is why clicking the back button in Next.js feels instant — you're getting the cached RSC payload, not re-rendering on the server.
Opting Out of the Router Cache
Sometimes you want the page to always re-fetch from the server — for example,
after a user submits a form that changes data. Call router.refresh()
to invalidate the Router Cache for the current route:
"use client";
import { useRouter } from "next/navigation";
export default function DeleteButton({ postId }: { postId: string }) {
const router = useRouter();
async function handleDelete() {
await fetch(`/api/posts/${postId}`, { method: "DELETE" });
router.refresh(); // Clears Router Cache and re-fetches server data
}
return <button onClick={handleDelete}>Delete Post</button>;
}
6. On-Demand Revalidation
Time-based revalidation is convenient but imprecise — data might be stale for up to the entire revalidation window. On-demand revalidation lets you invalidate the cache exactly when data changes — perfect for CMS content, e-commerce inventory, or any data you control.
There are two on-demand revalidation functions, both imported from
next/cache:
revalidatePath — Invalidate a Specific Route
import { revalidatePath } from "next/cache";
// Call this after data changes to clear the cache for a specific path
revalidatePath("/blog"); // Clears cache for /blog
revalidatePath("/blog/my-post"); // Clears cache for one specific post
revalidatePath("/blog/[slug]", "page"); // Clears ALL blog post pages
revalidateTag — Invalidate by Tag
import { revalidateTag } from "next/cache";
// Clears all cached fetch() responses tagged with "posts"
revalidateTag("posts");
Real-world Example — Revalidating After a Server Action
The most common use case: a user creates or updates content, and you want the public page to immediately reflect that change:
// app/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath, revalidateTag } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const body = formData.get("body") as string;
// 1. Save to database
const post = await db.post.create({
data: { title, body, published: true },
});
// 2. Invalidate the cache so the new post appears immediately
revalidatePath("/blog"); // Blog listing page
revalidatePath(`/blog/${post.slug}`); // The new post's page
revalidateTag("posts"); // Any fetch tagged with "posts"
// 3. Return the new post (optional)
return post;
}
// app/blog/page.tsx
// This fetch is tagged — revalidateTag("posts") will clear it
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
The moment createPost runs, the cache is cleared. The next request
to /blog re-renders with fresh data from the database. No waiting
for a time window to expire.
7. Route Segment Config — Controlling Caching at the Page Level
Instead of setting cache options on individual fetch() calls, you can
control caching for an entire route segment by exporting config variables from
your page.tsx or layout.tsx:
// app/dashboard/page.tsx
// Force this entire route to be dynamic (never cached)
export const dynamic = "force-dynamic";
// OR — revalidate every 60 seconds
export const revalidate = 60;
// OR — force static (cache forever)
export const dynamic = "force-static";
These are the available options:
| Export | Value | Effect |
|---|---|---|
dynamic |
"auto" |
Default — Next.js decides based on what the route uses |
dynamic |
"force-dynamic" |
Always render dynamically — never cache |
dynamic |
"force-static" |
Force static rendering — cache forever |
revalidate |
number |
Revalidate every N seconds |
revalidate |
0 |
Never cache — equivalent to force-dynamic |
revalidate |
false |
Cache forever — equivalent to force-static |
Route segment config is a blunt instrument — it applies to everything in the route. Prefer per-fetch cache options when you need granular control. Use route segment config when you want to make a blanket statement about an entire page (e.g. "this dashboard is always dynamic").
8. Choosing the Right Strategy — A Decision Guide
Here's how to pick the right caching strategy for any piece of data:
| Data type | Example | Strategy |
|---|---|---|
| Never changes | List of countries, currency codes | force-cache (cache forever) |
| Changes rarely | Blog posts, product descriptions | revalidate: 3600 + revalidateTag on update |
| Changes frequently | Comments, notifications, cart | revalidate: 60 or no-store |
| Real-time | Live scores, stock prices, chat | no-store or WebSockets |
| Per-user | Dashboard, profile, orders | no-store + force-dynamic |
| User triggers update | Create/edit/delete actions | revalidatePath or revalidateTag in Server Action |
9. Common Gotchas
-
Using
cookies()orheaders()makes the entire route dynamic. The moment you call either of these functions in a page or layout, Next.js opts the entire route out of the Full Route Cache. This is intentional — those values are per-request by nature — but it can surprise you if you call them in a layout that you expected to be cached. - Data Cache vs Full Route Cache are separate. You can have a route that's dynamically rendered (no Full Route Cache) but still benefits from the Data Cache for its fetch calls. Dynamic rendering doesn't mean no caching at all — it just means the HTML is generated fresh per request, while fetch responses may still be cached.
-
Revalidation in development is different. In
npm run dev, the Data Cache and Full Route Cache are effectively disabled — every request fetches fresh data. This is intentional for easier debugging. Always test caching behaviour with a production build (npm run build && npm start). -
revalidatePathclears the Full Route Cache, not the Data Cache. If you want to clear the underlying fetch data too, userevalidateTagalongsiderevalidatePath, or ensure your fetch usesno-store. -
The Router Cache cannot be fully disabled. You can call
router.refresh()to clear it for the current route, but it will re-populate on the next navigation. Accept it as a feature — it's what makes the back button feel instant.
Key Takeaways
- Next.js has four caching layers: Request Memoization, Data Cache, Full Route Cache, and Router Cache — each serving a different purpose.
- Request Memoization deduplicates identical fetches within one render — automatic and always on.
- The Data Cache persists fetch responses across requests — controlled via
cacheandnextoptions onfetch(). - The Full Route Cache stores pre-rendered HTML — routes are static by default unless they use dynamic functions or
no-store. - The Router Cache lives in the browser and makes navigation feel instant — cleared with
router.refresh(). - Use
revalidatePath()andrevalidateTag()in Server Actions to invalidate the cache the moment data changes. - Caching is disabled in development — test with a production build to see real behaviour.
Next up: Lesson 204 — Streaming with Suspense. You'll learn how Next.js sends HTML to the browser in chunks, how to architect pages for maximum streaming performance, and how to handle the edge cases that trip people up.