Next.js caching, finally explained
Four caches, one mental model. How the Data Cache, Full Route Cache, Router Cache, and Request Memoization actually behave, and how to make them do what you want.

Most "Next.js caching is confusing" complaints are really one complaint: there is not one cache, there are four, and they all kick in at different moments without asking permission. Once you can name them and know what invalidates each, the confusion goes away. This is the explanation we wish someone had handed us before we shipped a page that served three-hour-old data and swore it was a backend bug.
The four caches, in order of how often they bite you
Next.js (App Router, versions 13 through 15) has four separate caching layers. They are not alternatives to each other. They stack.
- Request Memoization: dedupes identical
fetchcalls inside a single render pass. - Data Cache: stores
fetchresults on the server, across requests and across users. - Full Route Cache: stores the rendered HTML and RSC payload for a route at build time.
- Router Cache: stores RSC payloads in the browser so back/forward navigation is instant.
The two that surprise people in production are the Data Cache (server-side, persistent) and the Router Cache (client-side, sticky). We will spend most of our time there.
Request Memoization is the one you can mostly ignore
If you call fetch("/api/user") three times while rendering one page, Next.js runs the network request once and hands the same result to all three callers. This lives for the duration of a single render. You do not configure it, it does not persist, and it only applies to fetch. The practical win: you can fetch the same data in a layout and a page without a second round trip, no prop-drilling required.
It only memoizes fetch. If you talk to Postgres with a raw client, wrap the function in React's cache() to get the same dedupe behavior:
import { cache } from "react";
import { db } from "@/lib/db";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});Now getUser("42") called in three components during one render hits the database once.
The Data Cache is where the real bugs live
This is the one that serves stale data and makes you question reality. By default, in Next.js 14 and earlier, fetch results are cached indefinitely on the server. Not for the request. Not for the user. Forever, until something invalidates them.
// Cached forever by default (Next 14 and earlier).
const res = await fetch("https://api.example.com/prices");That snippet, sitting in a Server Component, will happily serve the first response it ever got, weeks later, to every visitor. This is great for a list of countries and a disaster for a stock ticker.
You control it three ways, and you should pick one deliberately:
// 1. Time-based: revalidate at most every 60 seconds.
fetch(url, { next: { revalidate: 60 } });
// 2. Always fresh: never cache, run on every request.
fetch(url, { cache: "no-store" });
// 3. Tagged: cache until you explicitly invalidate the tag.
fetch(url, { next: { tags: ["prices"] } });The tag approach is the most useful one and the most underused. You attach a label to the cached data, then bust it on demand from a Server Action or route handler the moment the underlying data changes, the same tag-based invalidation idea behind a good Redis caching layer:
"use server";
import { revalidateTag } from "next/cache";
import { db } from "@/lib/db";
export async function updatePrice(id: string, value: number) {
await db.price.update({ where: { id }, data: { value } });
revalidateTag("prices"); // every fetch tagged "prices" is now stale
}This is the pattern we reach for constantly across our Next.js development work: cache aggressively, then invalidate precisely when a write happens. You get the speed of a fully cached read path without the staleness, because the only thing that clears the cache is the actual mutation.
A note on the moving target: Next.js 15 flipped the default. fetch and route handlers are no longer cached unless you opt in. That is a saner default, but it means a project on 15 behaves differently from the same code on 14. Always check which major version you are on before reasoning about defaults, because half the "caching bugs" we get called in for are really version-mismatch surprises.
The Full Route Cache: build-time HTML
When a route has no dynamic inputs (no cookies(), no headers(), no uncached fetch, no searchParams), Next.js renders it at build time and serves the same HTML to everyone, which is where the edge versus server rendering tradeoffs start to matter. That is the Full Route Cache. It is why a static blog post route costs you almost nothing per request.
You opt out per route when you genuinely need per-request rendering:
// In a page.tsx or layout.tsx
export const dynamic = "force-dynamic";But reach for that sparingly. A single uncached data dependency already tips the route into dynamic rendering. More often the fix is not "force dynamic," it is "cache the one slow fetch and let the rest of the page stay static."
The Router Cache: the sticky client-side one
This is the cache that makes people swear the page "did not update." After you navigate, Next.js keeps the RSC payload for visited routes in memory in the browser. Click away and click back, and you see the cached version, not a fresh fetch. The default lifetime for dynamically rendered pages is short (around 30 seconds), but for the user clicking around in one session it feels like nothing ever refreshes.
The fix is not client trickery. It is to invalidate from the server when you mutate, using the same revalidatePath or revalidateTag call inside the Server Action that performed the write. When the server tells the client a path is stale, the Router Cache for that path is dropped and the next navigation refetches.
"use server";
import { revalidatePath } from "next/cache";
export async function addComment(postId: string, body: string) {
await db.comment.create({ data: { postId, body } });
revalidatePath(`/posts/${postId}`); // drops Router + Data cache for this path
}If you mutate data and the UI does not move, the question to ask is almost never "why is React not re-rendering." It is "did I tell Next.js the cached path is stale."
A mental model you can actually keep in your head
Read paths cache by default (on older versions) and you opt out where freshness matters. Write paths must announce what they changed, by tag or by path, so the caches downstream of them get cleared. That is the whole game:
- Dedupe within a render with
fetchorcache(). - Cache reads on the server with
revalidatefor time-based freshness, ortagsfor precise control. - Bust on write with
revalidateTagorrevalidatePathinside the Server Action that did the mutation.
Get those three habits right and the four caches stop being a mystery box. They become exactly what you want: fast reads, and fresh data the instant something changes.
If you are staring at a Next.js app serving data older than it should, we are happy to take a look.
