Next.js Caching Strategies Explained
Walk through the four caching layers in Next.js App Router and learn how to choose static, ISR, dynamic, or per-request fetch caching.
What you'll learn
- ✓The four cache layers in Next.js
- ✓Difference between static, dynamic, and ISR
- ✓How fetch cache options work
- ✓revalidatePath and revalidateTag
- ✓Common debugging tactics
Prerequisites
- •Comfortable with HTML and JavaScript
What and Why
Caching is what makes Next.js fast, but it is also the source of most surprises. The framework caches at four different layers and each has its own knobs. Knowing where a response is being remembered helps you build pages that feel instant without serving stale data to users.
Mental Model
Picture a request walking through four checkpoints. First, the full route segment cache decides whether the whole page can be served from disk. Second, the React component tree may be reused from the data cache. Third, individual fetch calls inside server components may have memoized responses. Fourth, on Vercel, a CDN sits in front of everything. If you skip the first checkpoint, you may still hit the third one.
Request
-> CDN edge cache
-> Full route cache (HTML)
-> Data cache (RSC payload)
-> Fetch memoization (per request)
-> Origin (DB, API)
Hands-on Example
A page is fully static by default. Any fetch inside it is cached forever unless told otherwise.
// app/blog/page.tsx
export default async function Blog() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
To rebuild this page periodically without redeploying, use revalidation.
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
}).then(r => r.json());
Every minute the next request triggers a background regeneration. Users keep seeing the cached version until the new one is ready. This is incremental static regeneration in App Router form.
For data that must always be fresh, opt out entirely.
const user = await fetch(`/api/me`, { cache: 'no-store' }).then(r => r.json());
Tag-based invalidation lets you wipe related responses on a write.
const posts = await fetch('/api/posts', { next: { tags: ['posts'] } });
Inside a server action or route handler you can then call revalidateTag('posts') to mark every cached fetch with that tag as stale.
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
await db.posts.insert({ title: formData.get('title') });
revalidateTag('posts');
}
Common Pitfalls
The biggest gotcha is mixing dynamic functions with static fetches. The moment you call cookies(), headers(), or use searchParams, the route becomes dynamic and the full route cache is skipped. People then wonder why their revalidate: 60 setting is doing nothing. Check whether your page is static by running next build and looking for the route symbol.
Another trap is forgetting that fetch caching is keyed by the full URL plus headers. If you pass a different cache-busting query parameter on every request, you get a fresh response every time and the cache is useless.
In development the cache is disabled in many places to make iteration easier. Always test caching behavior with next build && next start.
Practical Tips
Use route segment config to set defaults for a whole subtree. Adding export const revalidate = 3600 at the top of a layout applies one hour ISR to every page below it.
Prefer tags over paths for invalidation. They are more precise and survive route refactors. Path-based revalidation is helpful for sitemap or feed updates where a single URL is involved.
When fetching from internal route handlers, use a fully qualified URL with process.env.NEXT_PUBLIC_SITE_URL. Relative URLs do not work in server components.
Log cache hits during development by wrapping fetch in a small helper that prints the x-vercel-cache header in production. You learn quickly which routes are dynamic by accident.
Wrap-up
The Next.js cache is a power tool. Once you can map a behavior to one of the four layers, debugging becomes mechanical. Start every route static, revalidate by tag, and reach for no-store only when the data is truly per-request. That order produces fast pages with predictable freshness.
Related articles
- Next.js Next.js Image Optimization Deep Dive
Learn how the next/image component handles responsive sizing, lazy loading, format conversion, and caching to ship faster sites.
- Next.js Next.js Route Handlers vs API Routes
Understand the difference between Pages Router API routes and App Router route handlers, including request, response, and runtime options.
- Next.js Next.js Streaming and Suspense Boundaries
Learn how the App Router streams HTML over the wire and how Suspense boundaries control what users see while data loads.
- CI/CD CI/CD Pipeline Caching Techniques
Speed up CI builds with dependency caches, layer caches, remote build caches, and content-addressed storage. Learn what to cache and what to skip.