Next.js

Next.js Caching & Revalidation👨‍💻

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.

Key Takeaways

  • 1Next.js has four cache layers: Request Memoization (per-render), Data Cache (persistent fetch results), Full Route Cache (server HTML + RSC payload), and Router Cache (client-side navigation cache)
  • 2Since Next.js 15, `fetch` is NOT cached by default -- you must explicitly opt in with `cache: 'force-cache'` or `next: { revalidate }`. This is the opposite of how it worked before.
  • 3Time-based revalidation (`next: { revalidate: 3600 }`) uses stale-while-revalidate: it serves the stale page instantly and regenerates in the background for the next request
  • 4On-demand revalidation with `revalidatePath()` and `revalidateTag()` lets you bust the cache immediately after a mutation -- use this for CMS webhooks and Server Actions
  • 5The `'use cache'` directive (Next.js 15+) extends caching beyond `fetch` to database queries, file reads, and any async computation. Pair it with `cacheTag()` for granular invalidation and `cacheLife()` for lifetime control
  • 6ISR (Incremental Static Regeneration) combines static performance with dynamic freshness -- pages are pre-rendered at build time and regenerated in the background after the revalidation window expires

Master next.js caching & revalidation

Take the Next.js Full-Stack course with hands-on lessons and challenges.

Examples

ISR for product pages -- static speed, fresh prices

tsx

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.

On-demand revalidation after CMS update via webhook

typescript

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.

Tag-based cache invalidation with fetch

typescript

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.

'use cache' directive for database queries

typescript

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.

Mixing static, cached, and dynamic content on one page

tsx

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.

fetch cache options -- the complete picture

typescript

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.

Common Mistakes

Mistake:

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

Fix:

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.

Mistake:

Using `revalidatePath('/')` to bust your entire site cache when only a single blog post changed

Fix:

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.

Mistake:

Putting `revalidatePath` or `revalidateTag` inside a Server Component render -- these functions only work inside Server Actions or Route Handlers

Fix:

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.

Mistake:

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

Fix:

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.

Mistake:

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

Fix:

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.

Best Practices

  • Combine time-based and tag-based revalidation as a belt-and-suspenders strategy: set `next: { revalidate: 3600, tags: ['products'] }` so data refreshes hourly at worst, but instantly when you call `revalidateTag('products')` after a mutation
  • Use granular cache tags with entity IDs (e.g., `post-${slug}`, `user-${id}`) rather than broad tags like `'data'`. This prevents a single update from busting unrelated cache entries across your app
  • Protect your revalidation Route Handler with a secret token (`x-revalidate-secret` header) -- without it, anyone can purge your cache and degrade performance
  • When migrating to Next.js 15, audit every `fetch` call and explicitly set caching behavior. The change from 'cached by default' to 'uncached by default' is the most impactful breaking change in the upgrade
  • Use `'use cache'` at the function level, not the file level, for fine-grained control. A file-level directive caches everything in the module, which may include functions that should always return fresh data
  • Place `<Suspense>` boundaries as close to dynamic content as possible when using Cache Components (PPR). This maximizes the static shell -- everything outside the boundary prerenders, and only the personalized piece streams at request time

Summary

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.

Practice Next.js with hands-on challenges

Learn next.js caching & revalidation hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Next.js with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.