Next.js App Router Caching: What Changed, What Breaks, and How I Handle It
Next.js caching in the App Router is powerful but confusing — especially after v15 flipped the defaults. Here's what the four caches actually do, what breaks silently, and the patterns I use in every production app.
If you've built anything with the Next.js App Router, you've hit this: you update data in the database, refresh the page, and nothing changes. Or the opposite — you expect a page to be fast and cached, but it's slow and rendering fresh on every request.
App Router caching is the most powerful feature in Next.js and also the most misunderstood. Part of the confusion is that v15 flipped the defaults entirely. What was opt-out before is now opt-in. If you migrated without auditing, you probably have pages that are either stale or unexpectedly slow.
This is what I've learned running it across seven production sites.
Next.js App Router has four distinct caches. They operate at different layers, and conflating them is where most bugs come from.
Request Memoization — deduplicates identical fetch calls within a single render tree. If two Server Components both call fetch('/api/user/123'), the request only fires once. This is automatic and you don't control it.
Data Cache — persists fetch results across requests and deployments. In v13/v14, this was on by default. In v15+, it's opt-in.
Full Route Cache — stores the rendered HTML and RSC payload of a route on the server. Static routes are fully cached here until you revalidate.
Router Cache — a client-side in-memory cache of RSC payloads. In v14, this cached Page components aggressively. In v15+, it no longer does by default.
You'll mostly interact with the Data Cache and Full Route Cache. The others are automatic.
Before v15, fetch was cached by default. Every fetch inside a Server Component behaved like this:
// v13/v14 implicit default — you never wrote this, Next.js applied itfetch('https://api.example.com/data', {
cache: 'force-cache',
});
This made pages fast out of the box, but caused the most common bug in the ecosystem: stale data that refused to update no matter how many times you refreshed.
Stale data after mutations (v14 and earlier). You update a row in your database via a Server Action. The page re-renders, but shows the old value. The Data Cache is still serving the previous fetch result. Without calling revalidatePath or revalidateTag after the mutation, the cache never knows anything changed.
Pages going dynamic unexpectedly. If any Server Component in a route uses cookies(), headers(), or reads searchParams, Next.js opts the entire route into dynamic rendering — bypassing the Full Route Cache, even if 95% of the page has nothing dynamic in it.
// This one line makes the whole route dynamicconst cookieStore = awaitcookies();
If you only need the cookie value in one small part of the layout, push that logic into a child Server Component. The parent route stays static; only the leaf that actually reads the cookie goes dynamic.
Tag mismatches. If you tag a fetch but the string doesn't match exactly when calling revalidateTag, nothing gets invalidated and you'll spend an hour wondering why your mutation isn't working.
// Tagging the fetchconst data = awaitfetch('/api/posts', {
next: { tags: ['posts'] },
});
revalidateTag('posts'); // ✓ worksrevalidateTag('post'); // ✗ does nothing — exact match required
For data that changes infrequently, I use a revalidate value on the fetch or at the route segment level:
// On a specific fetchconst data = awaitfetch('/api/config', {
next: { revalidate: 3600 },
});
// Or applied to all fetches in a route segmentexportconst revalidate = 3600;
For data that changes on user action, I tag fetches and invalidate the tag in the Server Action. If the data source is Supabase (not a native fetch), I wrap it with unstable_cache:
Marketing and content pages (/, /blog, /about): revalidate = 3600 or tag-based invalidation. Fast on first load, updates when content changes.
Dynamic content routes (/blog/[slug]): generateStaticParams to pre-render known paths at build time, revalidate for new posts.
Authenticated routes (/dashboard, /settings): force-dynamic. No point caching per-user data server-side.
All Server Actions that mutate: always call revalidatePath or revalidateTag at the end, without exception. Skipping this makes mutations look broken from the user's perspective even when they succeed.
The mental shift that made this click: caching in the App Router isn't magic you turn on — it's a contract between your data layer and your routes. You define how fresh data needs to be, and you honor that contract when data changes.
Once you think of it that way, the four caches stop feeling like gotchas and start feeling like knobs you actually want to turn.
WC
عن الكاتب
Wonsuk Choi
وونسوك تشوي مطور فل ستاك وباني منتجات ذكاء اصطناعي يركز على النمو العملي.
I already had an llms.txt. It was stale the moment I added the second blog post. Here's what I replaced it with — and what actually matters for AI search visibility.
A look inside the private Next.js dashboard I built to manage 13 live sites — site status monitoring, personal AI, offline-capable notes, multi-tenant architecture, and more.
عمل حر
هل تحتاج مساعدة في هذا؟
أساعد في الترحيل، المنتجات الجديدة، وتحسين الأداء.