Next.js — Caching Strategies
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 | Location | Duration | Purpose |
|---|---|---|---|
| Data Cache | Server | Persistent | Caches fetch results and function results |
| Full Route Cache | Server | Persistent | Caches rendered HTML/RSC output |
| Router Cache | Client | 30 seconds (session) | Caches RSC payloads in browser |
| Request Memoization | Server | Per request | Deduplicates fetch calls in same render |
| 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) |
| 5 | async 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 |
| 12 | async 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 |
| 20 | async 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 |
| 28 | async 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 |
| 39 | async 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 |
| 47 | export 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
| 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) |
| 5 | export 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 |
| 20 | export const dynamic = "force-dynamic"; |
| 21 | |
| 22 | export 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 |
| 36 | export const revalidate = 60; // Revalidate every 60 seconds |
| 37 | export const fetchCache = "force-cache"; // Cache all fetches on this page |
| 38 | export const runtime = "edge"; // Use Edge Runtime |
| 39 | export const preferredRegion = "auto"; // Edge region selection |
| 40 | |
| 41 | // Segment-level cache configuration |
| 42 | // app/dashboard/layout.tsx |
| 43 | export const dynamic = "force-dynamic"; // All routes under /dashboard are dynamic |
| 1 | // Time-based revalidation — revalidate after N seconds |
| 2 | async 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 |
| 11 | import { revalidateTag } from "next/cache"; |
| 12 | import { NextRequest, NextResponse } from "next/server"; |
| 13 | |
| 14 | export 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 |
| 29 | import { revalidatePath } from "next/cache"; |
| 30 | |
| 31 | export 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 |
| 46 | async 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": "..." } |
| 1 | // unstable_cache caches the result of any async function |
| 2 | // Useful for caching database queries, API calls, etc. |
| 3 | |
| 4 | import { unstable_cache } from "next/cache"; |
| 5 | |
| 6 | // Cache a database query |
| 7 | const 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 |
| 23 | export 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 |
| 43 | const 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 |
| 56 | const 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 |
| 66 | import { revalidateTag } from "next/cache"; |
| 67 | revalidateTag("products"); |
| 68 | revalidateTag("session"); |
| 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 |
| 5 | import 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"; |
| 21 | import { useRouter } from "next/navigation"; |
| 22 | |
| 23 | function 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 |
| 37 | import { prefetch } from "next/navigation"; |
| 38 | |
| 39 | // Prefetch a route programmatically |
| 40 | prefetch("/dashboard"); |
warning
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.