|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/caching
$cat docs/next.js-—-caching-strategies.md
updated Last week·16 min read·published

Next.js — Caching Strategies

Next.jsIntermediate to Advanced🎯Free Tools
Introduction

Next.js has multiple caching layers that work together to optimize performance. Understanding when each cache is used — and how to invalidate them — is critical for building fast, data-fresh applications. The main caches are the Data Cache (server-side), Full Route Cache (rendered output), and Router Cache (client-side).

Cache Layers Overview
CacheLocationDurationPurpose
Data CacheServerPersistentCaches fetch results and function results
Full Route CacheServerPersistentCaches rendered HTML/RSC output
Router CacheClient30 seconds (session)Caches RSC payloads in browser
Request MemoizationServerPer requestDeduplicates fetch calls in same render
Data Cache
data_cache.tsx
TSX
1// The Data Cache persists fetch results across requests
2// In Next.js 15+, fetch is NOT cached by default (opt-in with cache: "force-cache")
3
4// Default: no caching (fresh data every request)
5async function getProducts() {
6 const res = await fetch("https://api.example.com/products");
7 // equivalent to: fetch("...", { cache: "no-store" })
8 return res.json();
9}
10
11// Opt into caching
12async function getProductsCached() {
13 const res = await fetch("https://api.example.com/products", {
14 cache: "force-cache", // Cache the result
15 });
16 return res.json();
17}
18
19// Time-based revalidation
20async function getProductsWithRevalidation() {
21 const res = await fetch("https://api.example.com/products", {
22 next: { revalidate: 3600 }, // Revalidate every hour
23 });
24 return res.json();
25}
26
27// Cache with tags for on-demand revalidation
28async function getProductsByTag() {
29 const res = await fetch("https://api.example.com/products", {
30 next: {
31 tags: ["products", "inventory"],
32 revalidate: 3600,
33 },
34 });
35 return res.json();
36}
37
38// Cache based on dynamic segments
39async function getProduct(id: string) {
40 const res = await fetch("https://api.example.com/products/" + id, {
41 next: { revalidate: 60 }, // Revalidate every minute
42 });
43 return res.json();
44}
45
46// Server Component using cached data
47export default async function ProductsPage() {
48 const products = await getProductsCached();
49 return (
50 <div className="grid grid-cols-3 gap-4">
51 {products.map((product: any) => (
52 <ProductCard key={product.id} product={product} />
53 ))}
54 </div>
55 );
56}

info

In Next.js 15+, fetch() is not cached by default. Add { cache: "force-cache" } or { next: { revalidate: N } } to opt into caching.
Full Route Cache
route_cache.tsx
TSX
1// The Full Route Cache stores the rendered HTML and RSC payload
2// Static routes are cached automatically at build time
3
4// This route is fully cached at build time (static rendering)
5export default async function StaticPage() {
6 const data = await fetch("https://api.example.com/data", {
7 cache: "force-cache",
8 });
9 // The entire page output is cached on the server
10
11 return (
12 <div>
13 <h1>Static Page</h1>
14 <p>Data: {JSON.stringify(data)}</p>
15 </div>
16 );
17}
18
19// Opt out of Full Route Cache with dynamic rendering
20export const dynamic = "force-dynamic";
21
22export default async function DynamicPage() {
23 const data = await fetch("https://api.example.com/data");
24 // This page renders fresh on every request
25 // No Full Route Cache is used
26
27 return (
28 <div>
29 <h1>Dynamic Page</h1>
30 <p>Random: {Math.random()}</p>
31 </div>
32 );
33}
34
35// Per-page cache configuration
36export const revalidate = 60; // Revalidate every 60 seconds
37export const fetchCache = "force-cache"; // Cache all fetches on this page
38export const runtime = "edge"; // Use Edge Runtime
39export const preferredRegion = "auto"; // Edge region selection
40
41// Segment-level cache configuration
42// app/dashboard/layout.tsx
43export const dynamic = "force-dynamic"; // All routes under /dashboard are dynamic
Time-Based & On-Demand Revalidation
revalidation.ts
TSX
1// Time-based revalidation — revalidate after N seconds
2async function getProducts() {
3 const res = await fetch("https://api.example.com/products", {
4 next: { revalidate: 3600 }, // Revalidate every hour
5 });
6 return res.json();
7}
8
9// On-demand revalidation using cache tags
10// app/api/revalidate/route.ts
11import { revalidateTag } from "next/cache";
12import { NextRequest, NextResponse } from "next/server";
13
14export async function POST(request: NextRequest) {
15 const { tag, secret } = await request.json();
16
17 if (secret !== process.env.REVALIDATION_SECRET) {
18 return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
19 }
20
21 // Revalidate all entries tagged with this tag
22 revalidateTag(tag);
23
24 return NextResponse.json({ revalidated: true, tag });
25}
26
27// Revalidate specific paths
28// app/api/revalidate-path/route.ts
29import { revalidatePath } from "next/cache";
30
31export async function POST(request: NextRequest) {
32 const { path, type, secret } = await request.json();
33
34 if (secret !== process.env.REVALIDATION_SECRET) {
35 return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
36 }
37
38 // type: "layout" revalidates layout + all child routes
39 // type: "page" revalidates only the specific page
40 revalidatePath(path, type || "page");
41
42 return NextResponse.json({ revalidated: true, path });
43}
44
45// Using tags in fetch calls
46async function getProduct(id: string) {
47 const res = await fetch("https://api.example.com/products/" + id, {
48 next: { tags: ["products", "product-" + id] },
49 });
50 return res.json();
51}
52
53// Trigger revalidation from webhook (e.g., CMS update)
54// When CMS content changes, call the revalidate endpoint
55// POST /api/revalidate { "tag": "products", "secret": "..." }
unstable_cache
unstable_cache.ts
TSX
1// unstable_cache caches the result of any async function
2// Useful for caching database queries, API calls, etc.
3
4import { unstable_cache } from "next/cache";
5
6// Cache a database query
7const getProducts = unstable_cache(
8 async (category?: string) => {
9 const products = await db.product.findMany({
10 where: category ? { category } : undefined,
11 orderBy: { createdAt: "desc" },
12 });
13 return products;
14 },
15 ["products-list"], // Cache key prefix
16 {
17 revalidate: 3600, // Revalidate after 1 hour
18 tags: ["products"], // Tags for on-demand revalidation
19 }
20);
21
22// Usage in Server Component
23export default async function ProductsPage({
24 searchParams,
25}: {
26 searchParams: Promise<{ category?: string }>;
27}) {
28 const { category } = await searchParams;
29 const products = await getProducts(category);
30 // Results are cached per category value
31 // Different category = different cache entry
32
33 return (
34 <div>
35 {products.map((product) => (
36 <ProductCard key={product.id} product={product} />
37 ))}
38 </div>
39 );
40}
41
42// Cache user session
43const getUserSession = unstable_cache(
44 async (token: string) => {
45 const session = await db.session.findUnique({
46 where: { token },
47 include: { user: true },
48 });
49 return session;
50 },
51 ["user-session"],
52 { revalidate: 300, tags: ["session"] } // 5 minutes
53);
54
55// Cache API responses with different keys
56const getGitHubRepos = unstable_cache(
57 async (org: string) => {
58 const res = await fetch("https://api.github.com/orgs/" + org + "/repos");
59 return res.json();
60 },
61 ["github-repos"],
62 { revalidate: 600, tags: ["github"] }
63);
64
65// Revalidate specific cache entries
66import { revalidateTag } from "next/cache";
67revalidateTag("products");
68revalidateTag("session");
Router Cache (Client-Side)
router_cache.tsx
TSX
1// The Router Cache stores RSC payloads in the browser
2// It automatically prefetches and caches routes the user might visit
3
4// Prefetching — happens automatically for visible links
5import Link from "next/link";
6
7// This link is prefetched when it enters the viewport
8<Link href="/products">Products</Link>
9
10// Disable prefetching
11<Link href="/products" prefetch={false}>Products</Link>
12
13// Router cache behavior:
14// - Layouts: cached for the entire session (navigation doesn't re-fetch)
15// - Pages: cached for 30 seconds (then re-fetches on navigation)
16// - Static routes: cached permanently (never re-fetch)
17// - Dynamic routes: cached for 30 seconds
18
19// Force fresh data by re-fetching
20"use client";
21import { useRouter } from "next/navigation";
22
23function RefreshButton() {
24 const router = useRouter();
25
26 return (
27 <button onClick={() => router.refresh()}>
28 Refresh Data
29 </button>
30 );
31}
32
33// router.refresh() invalidates the Router Cache
34// and re-fetches data from the server
35
36// Prefetch from JavaScript
37import { prefetch } from "next/navigation";
38
39// Prefetch a route programmatically
40prefetch("/dashboard");

warning

The Router Cache is client-side only and resets on page refresh. For server-side caching, use the Data Cache with force-cache or revalidate.
Best Practices

1. Use { next: { revalidate: N } } for data that changes periodically but doesn't need to be real-time.

2. Use cache tags with revalidateTag() for on-demand updates when content changes in a CMS or database.

3. Use unstable_cache for caching database queries and API calls that are used across multiple components.

4. Set export const dynamic = "force-dynamic" for pages that must never be cached (user dashboards, real-time data).

5. Use router.refresh() to invalidate the client-side Router Cache when data changes.

6. Monitor cache hit rates in production — use logging to verify your caching strategy is effective.

$Blueprint — Engineering Documentation·Section ID: NEXT-CACHE·Revision: 1.0