Next.js caching cheatsheet.

The four caches

  1. Request memoization (per request, dedupe).
  2. Data cache (server, persistent, between requests).
  3. Full route cache (server, persistent, full HTML/RSC payload).
  4. Router cache (client, in-memory, for back/forward navigation).

Request memoization

Within a single request, identical fetch calls hit cache:

async function User() {
  const u = await fetch("/api/user").then(r => r.json());   // hits network
}

async function Header() {
  const u = await fetch("/api/user").then(r => r.json());   // memoized
}

Use cache() for non-fetch:

import { cache } from "react";

export const getUser = cache(async (id: number) => db.user.find(id));

Data cache (fetch options)

fetch(url, { cache: "force-cache" });           // cache forever
fetch(url, { cache: "no-store" });              // never cache
fetch(url, { next: { revalidate: 60 } });       // ISR: 60s
fetch(url, { next: { tags: ["users"] } });      // tag-based revalidate

Full route cache

Static routes (no dynamic functions) are cached as the rendered RSC payload + HTML. Invalidated by:

  • revalidatePath()
  • revalidateTag()
  • revalidate time
  • New deploy

Dynamic routes (using cookies, headers, params with dynamic = "force-dynamic") skip this.

Router cache (client)

When you <Link> to a route, the response is cached in memory for 30s (dynamic) or 5min (static, before Next 15 default). Back/forward use cache.

Invalidate:

import { useRouter } from "next/navigation";
const router = useRouter();
router.refresh();        // re-fetches RSC for current route

Triggering revalidation

"use server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function updateUser(id: number, data: any) {
  await db.user.update({ where: { id }, data });
  revalidatePath(`/users/${id}`);
  revalidateTag("users");
}

Opting out

Per-route:

export const dynamic = "force-dynamic";

Per-fetch:

fetch(url, { cache: "no-store" });

Per-request (via dynamic function use):

import { cookies } from "next/headers";
await cookies();      // route becomes dynamic

ISR with on-demand revalidation

export const revalidate = 3600;          // 1 hour
// API route to trigger revalidate
export async function POST(req: Request) {
  const { secret, path } = await req.json();
  if (secret !== process.env.REVAL_SECRET) {
    return NextResponse.json({ error: "bad" }, { status: 401 });
  }
  revalidatePath(path);
  return NextResponse.json({ revalidated: true });
}

Webhook from CMS → revalidate page.

unstable_cache (persistent non-fetch)

import { unstable_cache } from "next/cache";

export const getTopPosts = unstable_cache(
  async () => db.post.findMany({ orderBy: { views: "desc" }, take: 10 }),
  ["top-posts"],
  { revalidate: 300, tags: ["posts"] },
);

For DB queries that should persist between requests like fetch.

Cache invalidation

revalidatePath("/posts");           // exact path
revalidatePath("/posts", "page");   // also re-invalidate the path's layout
revalidatePath("/[id]", "layout");  // entire layout subtree
revalidateTag("posts");             // any cache entry with this tag

staleTime / nextRevalidate per page

// Static page with auto-revalidate
export const revalidate = 60;

Compare with dynamic = "force-dynamic" — total skip of caching.

Cache hit/miss in dev

NEXT_PRIVATE_DEBUG_CACHE=1 next dev

Logs each cache decision.

Common patterns

Static blog: export const revalidate = 3600; per post. CMS webhook calls revalidatePath on publish.

User dashboard: dynamic = "force-dynamic"; everywhere, or use cookies() to auto-trigger.

Public API proxy: tag responses, revalidate on backend events.

Common mistakes

  • force-cache on per-user data — returns wrong user’s data.
  • Forgetting revalidate* after mutation — UI stays stale.
  • Heavy unstable_cache without bounds → cache grows.
  • Mixing revalidate and dynamic = "force-dynamic" — contradictory.
  • Cache layer skipped because of cookies() but not realizing.

Read this next

If you want my caching patterns + webhook revalidator, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .