Lesson 02 of 04
The four caches, and which one is lying to you
Next.js has several independent cache layers. Almost every "why is my data stale" bug is one specific layer, and naming it makes the fix obvious.
OutcomesAfter this you will be able to
- Name the four cache layers and what each one keys on
- Diagnose stale data by identifying which layer served it
- Choose between time-based and tag-based revalidation deliberately
01The layers
From the database outward, a response passes through up to four caches. They are independent — clearing one does nothing to the others, which is exactly why stale-data bugs feel unfixable until you know which one you are fighting.
- Request memoization — dedupes identical `fetch` calls within a single render pass. Lives for one request. You almost never need to think about it, and it is why calling the same fetch in three components is fine.
- Data cache — persistent, server-side, survives deploys unless invalidated. This is the one that stores your `fetch` results across requests.
- Full route cache — the rendered HTML and RSC payload for a static route, built at build time.
- Router cache — client-side, in memory, holds RSC payloads for visited routes so back/forward feels instant.
02Opting in explicitly
Because caching is now opt-in for data, you say what you want per call. Be explicit even when the default is what you want — the person reading this in six months should not have to remember which Next version it was written against.
// not cached — hits the origin every request
const live = await fetch(url);
// cached until something invalidates the tag
const stable = await fetch(url, { next: { tags: ['products'] } });
// cached, refreshed at most once a minute
const timed = await fetch(url, { next: { revalidate: 60 } });
// explicitly never cached, stated out loud
const fresh = await fetch(url, { cache: 'no-store' });For data that does not come from `fetch` — a direct database query, say — `fetch` options do nothing. Wrap it in `unstable_cache` (or `"use cache"` where available) to get the same tagging behaviour, or accept that it runs every time.
03Tags beat timers
Time-based revalidation is a guess: you are betting that sixty seconds of staleness is acceptable and that a refetch every sixty seconds is affordable. Tag-based revalidation is a fact: the data changed, so the cache is wrong now.
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function publishPost(id: string) {
await db.update(posts).set({ published: true }).where(eq(posts.id, id));
revalidateTag('posts'); // every cached read tagged 'posts'
revalidatePath('/blog'); // and the rendered route itself
}Use a timer only when you do not control the writer — a third-party API, a feed you poll. If your own code performs the mutation, you know exactly when the cache became wrong, so say so.
04Debugging stale data
Work outward from the database. The layer that is lying is the first one where the value is wrong.
- Query the database directly. If the value is wrong here, it is not a cache problem at all.
- Hit the API route or server function with curl, bypassing the browser. Wrong here → data cache.
- Hard-reload the page. Correct after a hard reload but stale on client navigation → router cache.
- Correct in dev, stale in production → full route cache; the route was prerendered at build time.
Exercise
Break it on purpose
Build a page that reads a counter from the database and a server action that increments it. Get it to display a stale value, then fix it three different ways: `revalidateTag`, `revalidatePath`, and `router.refresh()`. Note which one was actually necessary and why the other two also appeared to work.