Next.js 15 Caching Deep Dive: fetch, use cache, and Partial Prerendering
By Arash Latifi
Next.js 15 flips the caching model: uncached by default, explicit use cache, and Partial Prerendering. Architecture, code, and production pitfalls explained.
TL;DR: In Next.js 15 nothing is cached unless you say so. Think of caching as a warehouse with 4 sections (scratchpad, central warehouse, ready-made storefront, browser pocket). Shared data →
revalidate+tags+revalidateTag. Personal data →private. Pages → static shell from the warehouse + live holes streamed viaSuspense(that's PPR).
If you shipped on Next.js 14, you remember fetch caching everything by default and you had to add cache: 'no-store' to keep things fresh. Next.js 15 flips it: nothing goes to the warehouse unless you explicitly put it there. Tiny change, huge reduction in "why am I seeing stale data?!" bugs.
Let's break it down — no jargon, just a warehouse metaphor and practical code.
The Warehouse Has 4 Sections — Don't Mix Them Up
Forget "the cache." Next.js has four different storages. Think warehouse:
| Warehouse section | Where? | What it does |
|---|---|---|
| Scratchpad (Request Memoization) | one request only | You fetch the same URL twice in one render? Second read hits the scratchpad, not the network. |
| Central warehouse (Data Cache) | server, global | Data you marked with revalidate or use cache lives here. |
| Ready-made storefront (Full Route Cache) | server, global | The built HTML + RSC payload for static routes — pre-built at build time. |
| Browser pocket (Router Cache) | in the user's browser | RSC the browser already fetched, kept in memory for instant back-navigation. |
Classic support ticket: "I disabled the cache but still get stale data." You cleared the central warehouse, but the ready-made storefront is still serving old HTML.
Before vs. After
Before (Next.js 14): Everything went to the warehouse automatically. You had to opt out.
After (Next.js 15+): Nothing goes to the warehouse unless you opt in. Safer default.
No more accidental stale data. You have to be intentional.
use cache — Put Any Function in the Warehouse, Not Just fetch
Before, only fetch could be warehoused. Now use cache lets you warehouse any async function — even a DB query:
With tags and revalidation:
Heads up: If data is per-user (recommendations, cart), you must add private. Forget it and User A's data leaks to User B. That's a bad day.
Where unstable_cache Still Fits
On stable 15 without use cache yet, unstable_cache is your friend:
Difference: unstable_cache is server-only. use cache will eventually work on components too and plugs deeper into PPR.
PPR — Storefront + Live Holes (Super Simple)
PPR in one sentence: ship the static storefront pre-built, stream the live bits through holes.
Think of a shop window: the mannequins and shelves are pre-arranged (static), only the price tags that change per customer are left empty and filled at request time.
Before PPR you had to make the whole page either static or dynamic. With PPR you say: shell from the warehouse, holes live.
Enable it in next.config.ts:
Result: near-static TTFB with real personalization. Without PPR you'd mark the whole page dynamic and throw away the speed win.
Production Gotchas — Learn These Once
1. Touching cookies() / headers() makes the whole route dynamic. Call them at the top level of a Server Component and your "static" page becomes dynamic. Fix: push that logic inside Suspense.
2. revalidateTag without tags does nothing. If you warehoused without a tag, busting by tag is a no-op. Always add tags when you cache.
3. The browser pocket will fool you. After revalidateTag, the user may still see stale data because their Router Cache (browser pocket) still holds it. Hard refresh or router.refresh() to verify.
4. Don't forget private for personal data. Rule of thumb: if it depends on the user, mark it private and test that User A never sees User B's stuff.
Quick Checklist
- [ ] On Next.js 15, assume nothing is warehoused unless you explicitly mark it.
- [ ] Shared data:
revalidate+tags+ on-demandrevalidateTag. - [ ] Personal data:
use cache: privateorunstable_cachekeyed byuserId. - [ ] Build pages as static shell +
Suspenseholes with PPR. - [ ] Keep
cookies()/headers()out of the top-level page — move intoSuspense.
Want to see a real fast, SEO-ready build using these patterns? Browse our projects — most are built exactly like this.
Questions about migrating from the old model? I'm Arash Latifi — ping me via contact and we'll map it to your project.
For more practical deep dives, check the tech archive.