EmpireUI
Get Pro
← Blog9 min read#next.js#caching#performance

Next.js Caching Deep Dive: Request, Data, Full Route and Client Cache

Next.js ships four distinct caching layers. Most developers only half-understand two of them — here's how all four actually work, when they conflict, and how to control them.

server rack with glowing cache storage drives in dark room

Why Next.js Caching Trips Everyone Up

Next.js has four caching layers that all run simultaneously, often interact with each other, and each have their own opt-out APIs. Stale data in production, pages that won't update no matter how many times you deploy, fetch calls that seem to cache when you told them not to — if you've hit any of those, you've already lost a fight with one of these layers.

The four are: Request Memoization, Data Cache, Full Route Cache, and Router Cache. The App Router (introduced properly in Next.js 13, stable in 14, and refined through 15) leans on all four at once. The Pages Router only really had ISR (Incremental Static Regeneration) and getStaticProps revalidation, so if you're migrating, the mental model shift is significant.

Honestly, the official docs explain each layer in isolation reasonably well. What they don't do clearly is show how they stack, and which layer wins when two of them disagree. That's what this article is actually about.

Worth noting: none of this is unique to Vercel hosting. These caches run inside the Node.js process (or edge runtime) wherever you deploy. You can hit all four on a Railway container or a bare VPS running next start.

Layer 1 — Request Memoization (Per-Render, In-Memory)

Request Memoization is the narrowest cache. It deduplicates identical fetch() calls made during a single server render. If ten different React Server Components call fetch('https://api.example.com/user/1') while rendering one page, the network request goes out exactly once. The remaining nine reads hit an in-memory store that lasts only for the duration of that render tree.

This is React's feature, not Next.js's. Next.js patches the native fetch to hook into it, but the deduplication logic ships with React 18's server rendering pipeline. You can't disable it for fetch. If you're using something that isn't fetch — like a direct database call or an SDK that uses axios internally — you won't get memoization unless you manually wire it up with React.cache().

import { cache } from 'react';

// wrapping a DB call so RSCs can share it without N queries
export const getUser = cache(async (id: string) => {
  return db.query('SELECT * FROM users WHERE id = $1', [id]);
});

One more thing — this cache is completely isolated per request. Two concurrent users rendering the same page each get their own memoization store. There's no cross-user data leakage here, which is the exact opposite of what you need to worry about with the Data Cache.

That said, Request Memoization is the one you think about the least. It just works. The ones below are where things get interesting.

Layer 2 — Data Cache (Persistent, Cross-Request)

The Data Cache is where most developers get burned. It's a persistent server-side cache that survives across requests, across deployments (on some hosts), and across users. When you call fetch() in a Server Component without any cache-busting options, Next.js caches that response indefinitely in the Data Cache. That default changed between Next.js 14 and 15.

In Next.js 14, fetch() defaulted to cache: 'force-cache'. In Next.js 15, the default flipped to cache: 'no-store'. If you upgraded and suddenly all your pages started making live fetch calls on every request, that's why. Check your next.config and component fetch calls if you're running 15+.

// Next.js 14 default — cached indefinitely
const data = await fetch('https://api.example.com/posts');

// Explicit revalidation — cache for 60 seconds, then revalidate
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 },
});

// Opt out entirely — always fresh
const data = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
});

// Tag it so you can invalidate on demand
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

On-demand revalidation via revalidateTag('posts') or revalidatePath('/blog') flushes entries from the Data Cache immediately. This is the pattern you want for CMS-driven content where editors trigger a webhook on publish. The tag-based approach is more surgical — you can bust only the data for affected pages rather than nuking everything.

Quick aside: the Data Cache is stored on the filesystem when you run next build + next start locally, but on Vercel it lives in a distributed cache tied to your deployment. That's why you can't always manually inspect it. If you're on a self-hosted setup, the cache files live under .next/cache/fetch-cache.

Layer 3 — Full Route Cache (Build-Time Static Pages)

The Full Route Cache stores the rendered HTML and RSC payload for statically generated routes. At build time, Next.js renders pages that don't use dynamic APIs (like cookies(), headers(), or searchParams) and saves the output. Subsequent requests get that pre-rendered response without touching your server code at all. This is the successor to getStaticProps — except it's automatic.

Whether a route ends up in the Full Route Cache depends on whether it's static or dynamic. A route becomes dynamic the moment you read from cookies(), headers(), searchParams, or any fetch with cache: 'no-store'. Even one dynamic fetch call inside one component in the tree can pull the entire route out of the Full Route Cache. Use the dynamic export to force the behavior explicitly when you need predictability.

// Force this route to always be static, even if something smells dynamic
export const dynamic = 'force-static';

// Force always dynamic — no Full Route Cache entry generated
export const dynamic = 'force-dynamic';

// Set a revalidation time for the whole route (ISR-style)
export const revalidate = 3600; // seconds

In practice, you'll debug Full Route Cache issues by running next build and checking the output. Routes are marked with (static), ƒ (dynamic), or (ISR). If a route you expected to be static shows as dynamic, something in the tree is opting out. The --debug flag on next build gives you more detail about which fetch or dynamic API triggered it.

Look, the Full Route Cache is the most impactful performance win in Next.js. A CDN serving pre-rendered HTML beats any server response time. Design your data access patterns to keep as many routes as possible static, and punt dynamic behavior to client components or route segments that genuinely need it.

Layer 4 — Router Cache (Client-Side, Browser Memory)

The Router Cache lives entirely on the client, in memory, in the browser tab. When you navigate to a page using the Next.js <Link> component, the RSC payload for that page gets cached in the Router Cache. Navigate away and come back, and Next.js serves it from memory — no round trip to the server. The page feels instantaneous.

There are two TTLs you need to know. Static routes are cached for 5 minutes. Dynamic routes are cached for 30 seconds. These are defaults as of Next.js 15.x and can be adjusted via staleTimes in next.config.js. If you're seeing a dynamic page that shows stale data when users navigate back to it, 30 seconds is probably the culprit.

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 0,   // disable Router Cache for dynamic routes
      static: 300,  // 5 minutes for static routes (default)
    },
  },
};

One thing that surprises a lot of people: the Router Cache is not invalidated by revalidatePath() or revalidateTag(). Those only flush the Data Cache and Full Route Cache on the server. The Router Cache gets cleared by a hard refresh, a full page navigation (not SPA navigation), or when the TTL expires. If you call a Server Action and the data changes, Next.js will automatically invalidate the Router Cache for the affected paths — but only if you called revalidatePath or revalidateTag inside that Server Action.

The Router Cache also prefetches pages linked from the current viewport. When you're on /, Next.js will quietly prefetch /about and /pricing if those links are visible. At 150ms of idle time the browser starts firing those prefetch requests. Users don't wait; the experience feels native-app fast. Just be aware that prefetched content also goes into the Router Cache with the same TTLs.

How All Four Layers Interact (And How to Debug Them)

Here's the flow when a user visits a page: Router Cache checked first. Cache hit? Serve immediately, done. Cache miss? Go to the server. Server checks Full Route Cache — static HTML already rendered? Serve it. Otherwise, run the React render. During that render, each fetch() call hits Request Memoization (deduplicated within this render) and then the Data Cache (persisted from previous renders). Responses bubble back up, HTML generates, gets stored in the Full Route Cache if the route qualifies, sent to the client, and stored in the Router Cache.

Debugging the stack looks like this in practice: add console.log to your data fetching functions and watch the server logs. If you see the log fire on every request, the Data Cache is opting out. If you never see it fire after the first request but the page still shows old data, something in the Full Route Cache or Router Cache is holding stale content. fetch responses include a x-nextjs-cache header in dev mode that tells you HIT, MISS, or REVALIDATED.

// Server Action that correctly busts all relevant caches
'use server';
import { revalidateTag } from 'next/cache';

export async function publishPost(id: string) {
  await db.posts.update({ id, published: true });
  revalidateTag('posts');        // busts Data Cache entries tagged 'posts'
  revalidateTag(`post-${id}`);   // busts the specific post's cache entry
  // Router Cache will also be invalidated for affected paths automatically
}

One more thing — you can inspect what's in the Data Cache on Vercel by checking the Cache section in your deployment's function logs. On self-hosted Next.js, the cache files under .next/cache/fetch-cache are named by the SHA of the request URL and options. You can rm -rf .next/cache/fetch-cache to manually clear it without redeploying, which is occasionally useful in staging environments.

That said, the cleanest mental model is: server-side caches (Data Cache, Full Route Cache) are reset by revalidate* calls and new deployments. Client-side cache (Router Cache) is reset by hard refresh, TTL expiry, or Server Action invalidation. Keep those two buckets separate in your head and most debugging sessions get much shorter. If you're building production-grade Next.js apps and want UI components that look just as polished as your data layer, browse the Empire UI component library — it's built with Next.js App Router in mind.

Practical Patterns: When to Use Which Cache Strategy

Marketing pages, docs, blog posts — let them be fully static. No dynamic APIs, time-based revalidate of maybe 3600 seconds, full ISR. Zero server load for the vast majority of traffic. If the content changes, fire a webhook that calls revalidatePath. This is the highest-leverage optimization in Next.js and it's essentially free.

For dashboards, user-specific pages, anything that reads from cookies() or headers(): accept that these are dynamic, use cache: 'no-store' on sensitive fetches, and lean on the Data Cache only for shared, non-user-specific data like configuration, feature flags, or public API responses. Tag everything so you can bust it surgically.

// Good pattern: split shared data (cached) from user data (not cached)
async function Dashboard({ userId }: { userId: string }) {
  // Shared config — cached, tagged, refreshes every 5 minutes
  const config = await fetch('/api/config', {
    next: { revalidate: 300, tags: ['config'] },
  }).then(r => r.json());

  // User-specific — never cached
  const userData = await fetch(`/api/users/${userId}`, {
    cache: 'no-store',
  }).then(r => r.json());

  return <DashboardUI config={config} user={userData} />;
}

E-commerce is the trickiest. Product pages can usually be ISR with a 60-second revalidate and on-demand invalidation when inventory or price changes. Cart, checkout, anything touching sessions — force dynamic. Product listing pages can be statically generated at build time if your catalog isn't enormous, then updated incrementally. Worth noting: the gradient generator and other tools on Empire UI follow this exact pattern — static shell, dynamic data fetched client-side for personalization.

If you're getting serious about Next.js performance, pair caching strategy with good component architecture. Server Components handle the heavy data fetching with proper cache tags. Client Components handle interactivity and real-time updates. Don't fight the model — the caching system is designed around it. Check out Empire UI's component library if you want pre-built patterns for common UI patterns that already follow App Router conventions.

FAQ

Why is my Next.js page still showing old data after I redeployed?

Most likely the Router Cache in the user's browser is still serving the old RSC payload. Hard refresh (Ctrl+Shift+R) clears it immediately. On the server side, a new deployment does clear the Full Route Cache, but if your data is fetched via a tagged fetch, the Data Cache entry might still be alive depending on your hosting setup. Call revalidateTag or revalidatePath after deploying to be safe.

What changed between Next.js 14 and 15 for caching?

The biggest breaking change is that fetch() now defaults to cache: 'no-store' in Next.js 15, where it previously defaulted to cache: 'force-cache'. If you upgraded and pages started making live fetch requests on every load instead of serving cached data, that's the reason. Audit your fetch calls and add explicit next: { revalidate: N } or next: { tags: [...] } options where you want caching back.

Does revalidateTag() also clear the Router Cache?

Not directly. revalidateTag() and revalidatePath() flush the Data Cache and mark Full Route Cache entries as stale. The Router Cache (client-side) will be invalidated automatically when you call these inside a Server Action — Next.js tracks which paths were affected and tells the client to refetch. But if you're calling these from an API route or external webhook, the client Router Cache won't know until the TTL expires or the user hard-refreshes.

How do I completely disable all caching during development?

In development mode (next dev), the Data Cache and Full Route Cache are effectively disabled — every request re-renders. The Router Cache still works client-side. For production debugging, you can set export const dynamic = 'force-dynamic' on a specific route, use cache: 'no-store' on all fetches, and set staleTimes: { dynamic: 0, static: 0 } in next.config.js. That nukes all four layers for the affected routes, though obviously don't ship that to prod.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js Caching in 2026: fetch, ISR, Dynamic and No-Store ExplainedNext.js Image Optimisation: next/image Deep Dive — Every Prop ExplainedWeb Font Loading in 2026: next/font, variable fonts and CLSServer-Side Streaming in React + Next.js: Suspense and RSC