Next.js

Frontend · Next.js

The Next.js Caching Model

Four different caches, four different lifetimes — why 'my data won't update' in the App Router almost always comes down to not knowing which one is holding the stale value.

9 min read
Next.jsCaching

Ask a room full of Next.js developers why a page won't show fresh data, and you'll get four different explanations — and they're often all plausible, because the App Router doesn't have one cache. It has four, stacked on top of each other, each with a different scope and a different lifetime. This is consistently one of the most confusing parts of the App Router, and the confusion isn't really about caching being hard — it's about four separate mechanisms sharing the word "cache" while doing completely different jobs.

Understanding them one layer at a time turns "why isn't this updating?" from a guessing game into a checklist. Here they are, in the order a request actually passes through them.

Layer 1: Request memoization

Start with the narrowest and shortest-lived of the four. If the same fetch call — same URL, same options — gets triggered by multiple components while rendering a single request, React deduplicates it. Only the first call actually hits the network; every other call in that same render pass reuses the pending promise's result.

This matters because it's common to fetch the same data — a user's profile, say — in a layout, a page, and a deeply nested component, all in the same tree. Without memoization that's three network calls for one page load. With it, it's one.

Scoped to one render, not persisted

Request memoization exists purely to avoid redundant work within a single render pass. It does not persist across separate requests, and it is never shared between different users. The moment the render finishes, the memoized result is gone — the next request starts from zero. This is the layer people mistake for "server-side caching," and it isn't one.

Layer 2: The Data Cache

This is the layer that actually persists across requests — and across different users. When you call fetch on the server, Next.js can cache the response on the server itself, keyed by the request, so the next request (from anyone) can reuse it without hitting the origin API again. This is controlled directly on the fetch call:

typescript
// Data Cache: cached across requests until revalidated
const res = await fetch("https://api.example.com/products", {
  next: { revalidate: 3600 }, // stale after 1 hour
});

// Data Cache: opt out entirely, always hit the network
const live = await fetch("https://api.example.com/inventory", {
  cache: "no-store",
});

// Request memoization still applies within this same render,
// even if both calls above happen again in a different component

revalidate: 3600 tells the Data Cache to keep serving the cached response for up to an hour before fetching a fresh copy — which is exactly what powers ISR-style incremental freshness for fetched data specifically, as opposed to the page as a whole. cache: "no-store" disables Data Cache entirely for that call, so it always goes to the network. You can also invalidate it on demand, ahead of the timer, by tagging the fetch and calling revalidateTag:

typescript
// Invalidate the Data Cache on demand, e.g. from a webhook
import { revalidateTag, revalidatePath } from "next/cache";

await fetch("https://api.example.com/products", {
  next: { tags: ["products"] },
});

// later, when the underlying data changes:
revalidateTag("products");
// or, to force the Full Route Cache to re-render a specific path:
revalidatePath("/products");

The Data Cache is where most "stale API data" bugs actually live, precisely because it's invisible from the component code — nothing about <ProductList /> tells you whether its fetch is hitting cache or the network.

Layer 3: The Full Route Cache

One level up from individual data fetches sits the route itself. For routes that can be statically determined — no dynamic functions like cookies() or searchParams forcing dynamic rendering — Next.js renders the route to HTML and an RSC payload once, typically at build time, and serves that pre-rendered output to every visitor. No re-render happens per request at all; the server is just handing back a file.

This is a different problem than the Data Cache solves. The Data Cache decides whether a fetch inside the route needs to hit the network again; the Full Route Cache decides whether the route itself needs to be rendered again at all. A route can have a perfectly fresh Data Cache entry and still serve a stale Full Route Cache snapshot from before that data changed, because the rendering step — turning data into HTML — was skipped entirely. That's why revalidatePath exists alongside revalidateTag: sometimes you need to force the route to re-render, not just force the fetch to refetch.

Layer 4: The Router Cache

The last layer lives entirely in the browser, not the server. As a user navigates around your app, the Next.js client keeps an in-memory cache of the route segments they've already visited. Hit the back button, or click a link to a page you were just on, and the client can paint it instantly from this cache instead of asking the server for anything.

It's short-lived and per-session — close the tab, and it's gone — but it's also the layer most likely to make a page feel stale even when the server has fully fresh data, because the browser never asked the server in the first place. Navigating away and back within the cache's window can show you exactly what was on screen a minute ago, changes and all.

Four caches, four lifetimes

Laid end to end, a single request passes through all four: request memoization dedupes identical fetches within that one render; the Data Cache decides whether those fetches hit the network or a server-side cache entry; the Full Route Cache decides whether the route needs rendering at all; and the Router Cache decides whether the browser even asks the server. Each one has its own scope (single render vs. server-wide vs. browser-local) and its own lifetime (one pass vs. a revalidation window vs. build time vs. a session), and none of them know about each other.

That's the actual source of almost every "why isn't my data updating?!" moment in a real App Router project. It is essentially never that Next.js caching is "broken" — it's that the value is stale in one specific layer, and the fix someone reached for (a hard refresh, a different fetch option, a revalidatePath call) targets a different layer than the one actually holding the stale data.

Debug in order, don't guess

When data looks stale, walk the four layers in the order a request hits them rather than randomly trying fixes: (1) is this the same fetch being deduped within one render — memoization? (2) is the Data Cache serving an old response because it hasn't revalidated yet? (3) is the Full Route Cache serving a pre-rendered page from before the data changed, so even a fresh Data Cache entry never gets rendered? (4) is the browser's Router Cache just showing you what was on screen before you navigated away? Fix the layer that's actually stale — a revalidateTag call does nothing if the Router Cache is the culprit, and a hard refresh does nothing if the Data Cache hasn't expired yet.

Key takeaways

  • Request memoization dedupes identical fetch calls within a single render pass — it never persists across requests or users.
  • The Data Cache persists fetch results across requests on the server, controlled by revalidate and tags — this is what actually powers ISR-style freshness for fetched data.
  • The Full Route Cache caches the rendered HTML/RSC payload for an entire static route, often at build time, so rendering itself can be skipped.
  • The Router Cache lives in the browser and makes back/forward and repeat navigation feel instant by skipping the server round trip entirely.
  • These four caches have different scopes and lifetimes and know nothing about each other — most stale-data bugs come from fixing the wrong layer.
  • When data looks stale, check memoization, then the Data Cache, then the Full Route Cache, then the Router Cache — in that order.
2026 — Built by Arpit Mishra