@tank/nextjs-caching-guide
1.0.0Description
Deep guide to Next.js App Router caching layers, revalidation strategies, use cache directive, fetch cache options, static vs dynamic rendering, ISR, cache debugging, and version-specific behavior changes.
Triggered by
next.js cachingrevalidatePathrevalidateTagISRstale datause cache
Download
Verified
tank install @tank/nextjs-caching-guideNext.js Caching Guide
Core Philosophy
- Understand the stack, not just the API — Next.js has four distinct cache layers that interact. Fixing stale data requires knowing which layer is stale, not blindly adding force-dynamic everywhere.
- Static by default, dynamic by intent — Start with static rendering and opt into dynamic behavior only for request-specific data. Every dynamic opt-in has a performance cost.
- Invalidation is the hard problem — Caching data is easy. Knowing when to invalidate is where bugs live. Prefer tag-based invalidation over path-based — tags follow data, paths follow UI.
- Version-aware decisions matter — Next.js 14, 15, and 16 each changed caching defaults significantly. Know which version runs in production before debugging.
- Cache debugging is a first-class skill — Use response headers, verbose logging, and the static route indicator to verify cache behavior rather than guessing.
Quick-Start: Common Problems
"I updated data but the page still shows old content"
- Identify which cache layer is stale — check the decision tree below
- After mutations in Server Actions, call
revalidateTag('tag')orrevalidatePath('/path') - On the client after a Server Action, call
router.refresh()to clear Router Cache - Verify with
NEXT_PRIVATE_DEBUG_CACHE=1that revalidation fires -> Seereferences/four-cache-layers.mdandreferences/revalidation-strategies.md
"Everything is dynamic and slow"
- Check if
force-dynamicorcache: 'no-store'is set at a layout level — it cascades to all children - Move dynamic data reads to leaf components, keep parent routes static
- Use
revalidatewith a time interval instead of disabling caching entirely - For user-specific data alongside cached content, use Partial Prerendering or composition patterns
-> See
references/static-dynamic-rendering.md
"Which caching model — fetch options or use cache?"
| Next.js Version | Model | Key API |
|---|---|---|
| 14.x | fetch cache defaults to cached | fetch({ next: { revalidate, tags } }) |
| 15.x | fetch cache defaults to uncached | fetch({ cache: 'force-cache' }), unstable_cache |
| 16.x+ | Cache Components | 'use cache' directive, cacheLife, cacheTag |
-> See references/use-cache-directive.md and references/fetch-cache-options.md
"How do I cache database queries (not fetch)?"
- Next.js 16+: Add
'use cache'to the async function wrapping the query - Next.js 15: Wrap with
unstable_cache(fn, keys, { tags, revalidate }) - Deduplication only: Wrap with
React.cache(fn)for same-render dedup without persistence -> Seereferences/use-cache-directive.md
Decision Trees
"My page is stale" — Which Cache Layer?
| Symptom | Likely Layer | Fix |
|---|---|---|
| Stale after deploy | Full Route Cache (build-time HTML) | revalidatePath or redeploy |
| Stale after mutation (server) | Data Cache | revalidateTag in Server Action |
| Stale on client navigation | Router Cache | router.refresh() after mutation |
| Duplicate fetches in same render | Request Memoization | Working as intended (dedup) |
| Stale on hard refresh but fresh on soft nav | CDN/edge cache | Check Cache-Control headers |
revalidatePath vs revalidateTag
| Signal | Use |
|---|---|
| Data is shared across routes (products, posts) | revalidateTag — follows the data |
| Single specific page needs refreshing | revalidatePath — targets one route |
| CMS publishes content used in many pages | revalidateTag — one call, all routes |
| User profile page after profile edit | revalidatePath('/profile') |
| Unclear which routes use this data | revalidateTag — decoupled from routing |
Static vs Dynamic Rendering
| Signal | Rendering | Config |
|---|---|---|
| Content same for all users | Static | Default (no config needed) |
| Reads cookies, headers, searchParams | Dynamic | Automatic (detected) |
| Personalized but cacheable per-user | Dynamic + short revalidate | revalidate: 60 |
| Real-time data, never cache | Dynamic | cache: 'no-store' or force-dynamic |
| Mostly static with one dynamic section | Partial Prerendering | Suspense boundary around dynamic part |
Reference Index
| File | Contents |
|---|---|
references/four-cache-layers.md | Request Memoization, Data Cache, Full Route Cache, Router Cache — how each works, duration, interaction |
references/revalidation-strategies.md | Time-based revalidation, on-demand with revalidateTag/revalidatePath, ISR patterns, webhook-triggered invalidation |
references/use-cache-directive.md | Cache Components (Next.js 16+), use cache syntax, cacheLife profiles, cacheTag, updateTag, serialization, interleaving patterns |
references/fetch-cache-options.md | fetch() cache option, next.revalidate, next.tags, unstable_cache, route segment config (dynamic, fetchCache, revalidate) |
references/static-dynamic-rendering.md | Static vs dynamic rendering triggers, Partial Prerendering, generateStaticParams, streaming + caching interaction |
references/cache-debugging.md | NEXT_PRIVATE_DEBUG_CACHE, response headers, static route indicator, build output analysis, production verification |
references/version-migration.md | Next.js 14 vs 15 vs 16 caching defaults, migration paths, staleTimes config, breaking changes |
references/self-hosting-cdn.md | Cache-Control headers, CDN integration, expireTime config, custom cache handlers, edge caching patterns |