React & Next.js · 5 min read

Next.js Caching Explained: Four Layers and How They Interact

Request memoisation, data cache, full route cache and router cache - what each stores, how to invalidate it, and where stale content actually comes from.

By Praful Patel · Last updated
Next.js Caching Explained: Four Layers and How They Interact - article cover
Next.js Caching Explained: Four Layers and How They Interact - article cover

Short answer

Next.js has four independent caches, and “why is my content stale” is almost always a question about which one you are looking at. Request memoisation lives for one render. The Data Cache survives deploys. The Full Route Cache stores rendered HTML. The Router Cache lives in the user’s browser and is the one that makes a fix look like it did not work.

Each cache has a different lifetime, a different scope and a different invalidation mechanism. Learning which is which converts a class of mysterious bugs into ordinary ones.

The four layers

Cache Stores Lives on Lifetime Cleared by
Request memoisation Return value of fetch Server One render pass Automatic
Data Cache fetch results Server Persistent, across deploys revalidateTag, revalidatePath, time
Full Route Cache Rendered HTML and RSC payload Server Persistent Revalidation, redeploy
Router Cache RSC payload per route Client Seconds to minutes router.refresh(), hard reload

Request memoisation

Within a single render, two components calling fetch with the same URL and options produce one network request. This is why “fetch where you use it” is good advice rather than wasteful — you do not need to hoist data fetching to avoid duplicate calls.

It applies only to fetch. A direct database client call is not deduplicated automatically; React’s cache() wrapper gives you the same behaviour for arbitrary functions.

import { cache } from 'react';

// Called from three components in one render; queries the database once.
export const getUser = cache(async (id: string) => db.user.findUnique({ where: { id } }));

The Data Cache

This is the persistent one, and the one that surprises people: it survives redeploys. Shipping new code does not clear it.

// Time-based: re-fetch at most once an hour.
await fetch(url, { next: { revalidate: 3600 } });

// Tag-based: cached until something invalidates this tag.
await fetch(url, { next: { tags: ['products'] } });

// Never cached - always hits the origin.
await fetch(url, { cache: 'no-store' });

Tag-based revalidation is the one worth designing around, because it lets a publish event invalidate exactly what changed:

// In a webhook route handler, after the CMS reports a change.
revalidateTag('products');            // every fetch tagged 'products'
revalidatePath('/blog/[slug]', 'page');  // one dynamic route's pages

The Full Route Cache

If a route has no dynamic inputs, Next.js renders it at build time and serves the stored HTML. A route becomes dynamic — and opts out of this cache — as soon as it reads cookies(), headers() or searchParams, or uses cache: 'no-store'.

This is the mechanism behind a common complaint: a page that was static becomes dynamic because someone added a cookie read for a feature flag, and the whole route’s caching silently disappears. Check the build output — it labels every route static or dynamic, and that table is the fastest way to catch an accidental opt-out.

The Router Cache

This one lives in the browser and causes the most confusion, because it makes a correct server-side fix look ineffective. After a client-side navigation, Next.js keeps the RSC payload so going back is instant. Revalidating on the server does not reach it.

Clear it with router.refresh() after a mutation, or by returning a server action that calls revalidatePath. A hard browser reload also clears it — which is why “it works when I refresh” is the classic symptom of a Router Cache issue rather than evidence that the server is wrong.

Debugging stale content

Work down the layers in order:

  1. Does a hard reload fix it? Router Cache. Call router.refresh() after the mutation.
  2. Does it persist across a hard reload but a redeploy does not fix it? Data Cache. You need revalidateTag or a shorter revalidate.
  3. Is the whole page stale and the build log says the route is static? Full Route Cache. Either revalidate it on publish or make the route dynamic deliberately.
  4. Is it stale only in production? The dev server caches far less aggressively — never validate caching behaviour in development.

A workable default

  • Tag every CMS fetch with the content type it returns.
  • Add a webhook route that calls revalidateTag when the CMS publishes, and verify the endpoint’s signature — it is a public URL that triggers work.
  • Set a modest time-based revalidate as a backstop, so a missed webhook produces content that is an hour stale rather than permanently wrong.
  • Use no-store only for genuinely per-request data such as a logged-in user’s cart. Applied broadly it turns off caching everywhere.
  • Call router.refresh() after mutations that change what the current page displays.

The revalidation webhook is the same pattern used for headless CMS publishing — see Headless WordPress with Next.js. Where these caches physically live depends on the deployment target, covered in Deploying Next.js.

Questions people actually ask

Why is my page still stale after revalidating?
Most often the client Router Cache, which server-side revalidation does not touch. If a hard reload shows the correct content, that is the layer at fault – call router.refresh() after the mutation, or trigger the change through a server action that revalidates the path.

Does deploying clear the cache?
It clears the Full Route Cache but not the Data Cache, which persists across deploys by design. This catches people out: new code ships and still serves old data, because the underlying fetch results were never invalidated.

revalidatePath or revalidateTag?
Tags when several routes depend on the same data – invalidate once and everything using it updates. Paths when you know exactly which route changed. Tags scale better as the site grows, because you stop having to enumerate every page that happened to render a given piece of content.

Why does caching behave differently in development?
The dev server deliberately caches far less so changes appear immediately. Never draw conclusions about caching from next dev – test against a production build, locally or in a preview deployment.

How do I make one route always fresh?
Export const dynamic = 'force-dynamic' from the route, or use cache: 'no-store' on the fetches that must be current. Prefer the second: it keeps the rest of the route’s caching intact rather than opting the whole page out.

← Back to all insights