Caching is the single most confusing part of Next.js. The framework has four distinct cache layers that interact in non-obvious ways, the defaults changed between versions, and the documentation mixes legacy APIs with modern ones. If you have ever deployed a page that stubbornly shows stale data, or added cache: 'no-store' everywhere out of frustration, this guide is for you.
Here is the mental model that actually works: think of Next.js caching as a pipeline. A request flows through the Router Cache (client), then the Full Route Cache (server HTML/RSC), then the Data Cache (persisted fetch results), and finally Request Memoization (per-render deduplication). Each layer can serve a hit or pass through to the next. Your job is to decide, for each piece of data, how long it can be stale -- and how to bust the cache when it cannot.
Critical change in Next.js 15: fetch requests are no longer cached by default. Before 15, every fetch in a Server Component was automatically cached with force-cache semantics. Now you must explicitly opt in with cache: 'force-cache' or next: { revalidate: N }. This broke a lot of apps during the upgrade -- if your pages suddenly became slower after migrating, this is why.
Master next.js caching & revalidation
Take the Next.js Full-Stack course with hands-on lessons and challenges.
The top 100 products are pre-rendered at build time via generateStaticParams. Other product slugs are generated on-demand on first visit. After 60 seconds, the next request serves the stale page instantly while regenerating a fresh version in the background. Users never wait for the server.
Your CMS calls this Route Handler after publishing content. revalidateTag targets specific cache entries (a single post + the listing page), while revalidatePath with 'layout' busts everything under that path. The secret header prevents unauthorized cache purges.
Each fetch is tagged so you can invalidate precisely what changed. A single post update busts only that post and the listing page, not unrelated cached data. The time-based revalidate acts as a safety net in case the on-demand invalidation is missed.
The 'use cache' directive (Next.js 15+) caches any async function, not just fetch. This replaces the old unstable_cache API. cacheTag makes the result invalidatable by tag, and cacheLife sets the time-based expiration. Arguments to the function (categoryId) automatically become part of the cache key.
This is the Cache Components (PPR) pattern. The header prerenders statically. CompanyMetrics uses 'use cache' so it is included in the static shell but refreshed hourly. UserStats reads cookies, so it streams at request time behind a Suspense boundary. Users see the shell instantly while personalized data loads.
Since Next.js 15, fetch is uncached by default -- a major change from prior versions. Use force-cache for truly static data, next.revalidate for time-based freshness, next.tags for on-demand invalidation, and no-store (or just omit options) for always-fresh data. You can combine revalidate and tags as a belt-and-suspenders approach.
Assuming fetch is cached by default after upgrading to Next.js 15 -- your pages are suddenly slower because every fetch now hits the origin on each request
After Next.js 15, fetch is uncached by default. Add `cache: 'force-cache'` or `next: { revalidate: N }` explicitly to each fetch that should be cached. Audit every fetch in your codebase during the migration.
Using `revalidatePath('/')` to bust your entire site cache when only a single blog post changed
Use `revalidateTag` with specific tags instead. Tag your fetches with `next: { tags: ['post-123'] }` and call `revalidateTag('post-123')`. This is surgical -- only the affected cache entries are purged, not every page on your site.
Putting `revalidatePath` or `revalidateTag` inside a Server Component render -- these functions only work inside Server Actions or Route Handlers
Move revalidation calls into a Server Action (with `'use server'`) or a Route Handler (`route.ts`). Calling them during render does nothing or throws an error. They are meant for mutations, not reads.
Wrapping database queries in `unstable_cache` when `'use cache'` with `cacheTag` is available -- the old API is verbose and has footguns around cache key serialization
If you are on Next.js 15+ with `cacheComponents: true`, use the `'use cache'` directive instead. It automatically derives cache keys from function arguments, works with `cacheTag()` for invalidation, and is the official replacement.
Setting `revalidate = 1` on a high-traffic page thinking it means 'always fresh' -- it actually means the page regenerates on every request after 1 second, putting heavy load on your server
For truly dynamic data, skip ISR entirely and use `cache: 'no-store'` on the fetch, or wrap the component in `<Suspense>` and let it render at request time. ISR is for data that changes infrequently, not for real-time feeds.
Next.js caching operates across four layers: Request Memoization (per-render dedup), Data Cache (persistent fetch results), Full Route Cache (pre-rendered HTML/RSC), and Router Cache (client navigation). Since Next.js 15, fetch is uncached by default -- you must explicitly opt in. Use `next: { revalidate }` for time-based ISR, `revalidatePath`/`revalidateTag` for on-demand invalidation after mutations, and the `'use cache'` directive to cache database queries and other async work beyond fetch. The winning pattern for most production apps is to combine time-based revalidation with tag-based on-demand invalidation, so pages are always fast but can be updated instantly when content changes.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.