Next.js caching is powerful and confusing in equal measure. There are four distinct layers, each controlled by different APIs, with different lifetimes and invalidation mechanisms. Next.js 15 changed the defaults in a breaking way. This guide explains all four layers clearly.
The Breaking Change in Next.js 15
In Next.js 14, fetch calls in Server Components were cached by default. Calling fetch("https://api.example.com/data") would cache the response indefinitely unless you opted out with { cache: "no-store" }.
In Next.js 15, this default was reversed. Fetch calls are no longer cached by default. You must explicitly opt into caching. This was a breaking change -- if you upgraded from Next.js 14 to 15 and suddenly saw more API calls or database queries, this is why.
Understanding this change is the prerequisite for understanding the rest of caching in Next.js.
Layer 1: Request Memoization
What it does: Deduplicates identical fetch calls within a single server render.
Scope: A single request/render cycle only. Does not persist between requests.
How it works: If two Server Components in the same render tree both call fetch("https://api.example.com/user/123"), the second call returns the cached result from the first. Only one actual HTTP request is made.
This is React's built-in deduplication for fetch. It does not require any configuration. It simply works when you use fetch with the same URL and options.
Use case: When multiple components need the same data and you do not want to prop-drill it. Both can fetch independently, and the runtime deduplicates automatically.
Important: This only works with fetch, not with database queries or other async operations. For database query deduplication, use React's cache() wrapper:
import { cache } from "react";
import { getDatabase } from "@/lib/mongodb/client";
export const getUserById = cache(async (userId: string) => {
const db = await getDatabase();
return db.collection("users").findOne({ _id: new ObjectId(userId) });
});
Now getUserById("123") called from multiple components in the same render returns the same promise -- one database query total.
Layer 2: Data Cache
What it does: Persists fetch responses across requests.
Scope: Persists until explicitly invalidated or the process restarts.
How it works: When you call fetch with a cache option, Next.js stores the response in its Data Cache (on the filesystem in development, in memory or persistent storage in production).
// Cached indefinitely (until invalidated with revalidatePath or revalidateTag)
const data = await fetch("https://api.example.com/data", {
next: { tags: ["products"] },
});
// Cached for 60 seconds, then refetched in the background
const data = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});
// Not cached -- fresh on every request (the Next.js 15 default)
const data = await fetch("https://api.example.com/data", {
cache: "no-store",
});
Invalidation: Call revalidatePath("/products") to invalidate all cached data used by the /products route. Call revalidateTag("products") to invalidate all fetch calls tagged with "products". Both can be called from Server Actions or Route Handlers after mutations.
// After creating a new product:
import { revalidateTag } from "next/cache";
revalidateTag("products");
Layer 3: Full Route Cache
What it does: Caches the pre-rendered HTML and RSC payload for static routes.
Scope: Persists between deployments (until you redeploy or explicitly invalidate).
How it works: During next build, Next.js pre-renders routes that do not use dynamic functions (cookies(), headers(), searchParams). The rendered HTML is stored and served directly for all requests, bypassing server rendering entirely.
A blog post at /blog/my-post with no dynamic functions is rendered once at build time and served as static HTML to every visitor. This is the fastest possible response -- no server computation per request.
When it applies: Static routes (no dynamic functions, no dynamic = "force-dynamic" export). Routes with revalidate set (Incremental Static Regeneration -- ISR).
Opt out: Add export const dynamic = "force-dynamic" to a route to always render on the server, never use the Full Route Cache.
Layer 4: Router Cache
What it does: Client-side cache of prefetched pages and visited routes.
Scope: Browser session only. Cleared on hard refresh, navigation to a new origin, or logout.
How it works: When a user navigates to a route, Next.js caches the RSC payload in memory in the browser. Navigating back to that route uses the cached version. Links that enter the viewport are prefetched and added to the Router Cache proactively.
Why it matters: Navigation between cached routes is instant. The server is not hit for routes already in the Router Cache.
The default durations: Static routes stay in Router Cache for 5 minutes. Dynamic routes stay for 30 seconds. These are the defaults as of Next.js 15.
Invalidation: Call router.refresh() from a client component to clear the Router Cache for the current route and refetch from the server. Calling revalidatePath or revalidateTag on the server also invalidates the client Router Cache for affected routes.
How the Layers Interact
A request for /products flows through the layers:
- Router Cache: Is this page in the client-side cache? If yes, return it instantly. If no, fetch from server.
- Full Route Cache: Is this a statically rendered page? If yes, return the pre-rendered HTML. If no, render on server.
- Data Cache: For each
fetchcall during rendering, is there a cached response? If yes, use it. If no, fetch and cache. - Request Memoization: Within this render, has this
fetchbeen called before? If yes, deduplicate.
Practical Caching Strategy
For most dynamic apps: Do not cache by default (the Next.js 15 default). Use revalidatePath/revalidateTag after mutations to bust what needs busting. Keep it simple.
For content-heavy sites (blogs, docs, marketing pages): Use the Full Route Cache. Pre-render at build time. Use ISR (revalidate: 3600) for content that updates occasionally.
For high-traffic data-heavy dashboards: Use the Data Cache with revalidate intervals for data that can tolerate staleness. Cache expensive computations with unstable_cache.
unstable_cache wraps any async function, not just fetch, with caching and revalidation. Despite the name, it is stable in practice:
import { unstable_cache } from "next/cache";
const getCachedProjectStats = unstable_cache(
async (orgId: string) => {
const db = await getDatabase();
return db.collection("projects").aggregate([...]).toArray();
},
["project-stats"], // cache key parts
{ revalidate: 300, tags: ["projects"] } // 5 min TTL, invalidatable by tag
);
Keep Reading
- Next.js App Router Patterns in 2026: What to Use and What to Avoid -- fetch caching in the App Router context
- React Server Components: What They Are and When to Use Them -- how RSC output relates to the Full Route Cache
- Next.js Performance Optimization: The Practical Guide for Production Apps -- caching as a performance tool
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace -- chat, projects, time tracking, AI meeting summaries, and invoicing -- in one tool. Try it free.