Next.js caching has changed more than any other part of the framework. Defaults flipped between versions, new APIs arrived, and the result is the most common production bug I see: stale data nobody can explain.
The layers
| Layer | Where | What it caches |
|---|---|---|
| Request memoisation | Server, one request | Identical fetches during a single render |
| Data cache | Server, across requests | Results of fetches/functions marked cacheable |
| Full route cache | Server | Rendered output of static routes |
| Router cache | Browser | Visited route segments during navigation |
The modern model: opt in with "use cache"
Newer Next.js versions move toward explicit caching: dynamic by default, cache what you choose.
import { cacheLife, cacheTag } from "next/cache"; export async function getProjects() { "use cache"; cacheLife("hours"); cacheTag("projects"); return db.project.findMany(); }
After a mutation:
"use server"; import { revalidateTag } from "next/cache"; export async function createProject(data: FormData) { await db.project.create({ /* ... */ }); revalidateTag("projects"); }
Debugging stale data
- Check the build output. Is the route static (○) or dynamic (ƒ)?
- Find the cache layer. Is it a cached function, a static route, or the browser router cache?
- Check revalidation. Does the mutation revalidate the right tag or path?
- Hard refresh. If a hard refresh fixes it, suspect the router cache.
A sane default strategy
- User-specific data: don't cache across users. Keep it dynamic.
- Shared, slow-changing data (blog posts, product lists): cache with tags, revalidate on change.
- External APIs with rate limits: cache with a time-based lifetime.
- Document it: a short
CACHING.mdlisting what's cached and how it's invalidated saves hours.