React

The four cache layers in Next.js

Memoization, Data Cache, Full Route Cache and Router Cache: what each layer keeps, for how long, and how you actually clear it.

Key points

  • The four caches have different keys: request, URL, route and client.
  • The build output says which routes are static: read it before blaming the browser.
  • `revalidatePath` clears the server, `router.refresh()` the browser: after a write you need both.

Next.js does not have a cache: it has four, with different keys and lifetimes. They are request memoization, the Data Cache, the Full Route Cache and the Router Cache. Mixing them up is the most common cause of stale data on a page.

The four caches, in order

LayerWhat it keepsLifetimeHow to clear it
Request memoizationResult of a fetch repeated in the same renderA single renderClears itself
Data CacheResponse of a fetch, keyed by URL and optionsUntil revalidate expiresrevalidateTag, revalidatePath
Full Route CacheHTML and RSC payload of a static routeUntil the next build or revalidationrevalidatePath, rebuild
Router CacheRSC payloads already visited, in the browserUp to 5 minutes on static routesrouter.refresh()

The build prints which route is static and which is dynamic. That line is the first thing to read when a value will not update: if the route is marked static, the browser is not the problem.

npm run build

Memoization is not a cache

Two calls with the same arguments in the same render produce one request. It does not outlive the response: there is nothing to invalidate and nothing to configure. For a database query you get the same effect with React cache(), which deduplicates the function for that render.

import { cache } from "react";
import { db } from "@/lib/db";

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

Data Cache: the key is the request

The Data Cache keeps the response of a server-side fetch. The key is the URL plus the options, so the same route called with a different header does not share the entry. With revalidate: 60 the response stays valid for a minute; with cache: "no-store" it never enters.

const res = await fetch(`https://api.example.com/products/${id}`, {
  next: { revalidate: 60, tags: [`product-${id}`] },
});

A tag is what makes the cache maintainable: you invalidate by name of the data, not by a list of URLs. The next call regenerates only the entries carrying that tag.

"use server";

import { revalidateTag } from "next/cache";

export async function updateProduct(id: string, data: ProductInput) {
  await db.product.update({ where: { id }, data });
  revalidateTag(`product-${id}`);
}

Full Route Cache: the whole page

A route without dynamic APIs is rendered at build time and served as HTML plus RSC payload. A single cookies(), headers() or server-side searchParams read makes it dynamic. That is not a switch to flip at random: it follows from which data the page uses.

Router Cache: what stays in the browser

The client keeps payloads of visited routes in memory, so the back button does not hit the server. After a write you need two separate gestures: revalidatePath clears the server cache, router.refresh() clears the browser one. The second does not update the database and the first does not touch the open tab.

This site as a measured case

This blog is the test bench: 299 JSON files, read on every request and served through a 5-second cache window in the Express server. The short window is a choice, not a default: with 299 posts the file system answers in milliseconds, and a long cache would hide a correction published minutes ago.

Where catalogue components fit

A UI that shows cached data needs empty states, skeletons and filters that live in the URL. The dashboard blocks cover the first case, the nuqs library the third, and the shadcn primitives the rest. They are interface pieces: the caching policy stays in the route.

Where it goes wrong

  • Caching an authenticated fetch without the user in the key: the second visitor receives the first one's data.
  • Using no-store everywhere for safety: you lose the cache and gain no protection.
  • Calling revalidatePath("/") for one record: the whole site is regenerated for a single row.
  • Trusting the Router Cache after a logout: the page stays in memory until you invalidate it.

One limitation to name

These caches live in the Node process. With two instances, revalidateTag clears the cache of the instance that receives the call and leaves the others with the stale entry: you need a shared cache handler, or a short expiry that makes the mistake harmless. The limit belongs to the infrastructure, not the framework.

To learn which route is static and which is not, start from structuring an App Router project; cache mistakes after a deploy are collected in 404 after deploy. If you are weighing another framework, Remix vs Next.js starts from the caching models.

Frequently asked questions

How many caches does Next.js have?

Four: request memoization, the Data Cache, the Full Route Cache and the Router Cache. Each has its own key and lifetime.

How do I clear the cache after an edit?

Use revalidateTag when the data carries a tag, revalidatePath when the whole route must go. For the browser, add router.refresh() after the mutation.

Why is my page always dynamic?

Because it reads cookies, headers or searchParams on the server, or uses an uncached fetch. Any one of those calls removes the Full Route Cache.

Does the Data Cache work with a database?

Not directly: it caches fetches. For queries use React cache() within the same render, or your own cache function with your own tags.

Do I need Redis for caching?

Not with a single instance: the process memory is enough. With several instances you need a shared handler, otherwise invalidation stays local.