Suggest an editImprove this articleRefine the answer for “Caching in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Next.js caching** works across four layers: request memoization (deduplicates identical fetches in one render pass), data cache (stores fetch responses across requests and deploys), full route cache (stores rendered HTML at build time for static routes), and router cache (client-side RSC payload per browser tab). ```tsx // Data cache: revalidate hourly const res = await fetch(url, { next: { revalidate: 3600 } }) // Force dynamic: no HTML caching export const dynamic = 'force-dynamic' ``` **Key:** `fetch` with `cache: 'no-store'` skips the data cache but not the full route cache. You need `dynamic = 'force-dynamic'` to disable full route caching.Shown above the full answer for quick recall.Answer (EN)Image**Next.js caching** stores fetch responses, rendered HTML, and route payloads across four layers so pages load fast without any custom caching logic written by hand. ## Theory ### TL;DR - Four layers: request memoization (deduplicates during one render), data cache (persists across requests and deploys), full route cache (stores HTML at build time), router cache (client-side, per browser tab) - Analogy: a restaurant kitchen with prep stations (memoization), a walk-in fridge (data cache), pre-plated meals (full route cache), and a waiter's notepad (router cache) - Server caches survive deployments; router cache dies when the tab closes - Decision rule: `cache: 'no-store'` for live data, default static for blogs and product catalogs - Biggest gotcha: `fetch` with `no-store` does NOT bypass the full route cache unless you also add `dynamic = 'force-dynamic'` ### Quick example ```tsx // app/posts/page.tsx // Two components fetch the same URL in one render pass // Network call happens exactly once (request memoization) async function PostList() { const res = await fetch('https://api.example.com/posts', { cache: 'force-cache' }) const posts = await res.json() return <ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul> } export default async function Page() { return ( <> <PostList /> {/* fetch runs here */} <PostList /> {/* no network call, reuses memoized result */} </> ) } ``` The second `<PostList />` reuses the already-fetched data. No extra HTTP request. This is request memoization at work. ### The four cache layers Next.js 14+ stacks caches in a specific order. Understanding which layer fires first prevents most interview mistakes. **Request memoization** runs during a single server render. React deduplicates `fetch` calls with the same URL and options by storing results in a per-request Map. It resets after the render is done. For Prisma or Drizzle queries that don't go through `fetch`, use `React.cache()` to get the same deduplication. **Data cache** persists across requests and deployments. Think of it as a CDN for your server-side fetch results. On Vercel it uses the Edge KV store; locally it writes to the filesystem. It survives server restarts unless you explicitly invalidate it with `revalidateTag()` or `revalidatePath()`. **Full route cache** stores the rendered HTML and RSC payload for static routes at build time. This is what makes a static Next.js page feel instant: the server serves a file, not a computation. A route stays fully static as long as no dynamic functions (`cookies()`, `headers()`, `searchParams`) are called and all fetches are cached. **Router cache** lives in the browser. It stores RSC payloads of routes the user has already visited, making back-navigation instant. It also prefetches `<Link>` targets automatically. The cache expires after 30 seconds of staleness or when the tab closes. ### Key difference: static vs dynamic rendering Static rendering means Next.js runs the page at build time (or on the first request for ISR), stores the HTML and RSC payload in the full route cache, and serves that file for every subsequent request. Good for blogs, product pages, marketing sites. Dynamic rendering is triggered by reading `cookies()`, `headers()`, or `searchParams`, or by setting `dynamic = 'force-dynamic'`. The server re-runs the component tree on every request. No full route cache involved. But the data cache still works inside dynamic routes. A page can re-render per request and still reuse cached API responses. These are two independent switches, and most teams treat them as one. ### When to use - **Static blog post** - default `fetch` or `next: { revalidate: 3600 }` (data cache, ISR) - **Ecommerce product page** - full route cache at build, `revalidateTag('product')` on stock update - **User dashboard** - `cookies()` or `{ cache: 'no-store' }` to force dynamic rendering - **Admin panel** - `export const dynamic = 'force-dynamic'` at the page level - **Live metrics** - `cache: 'no-store'` together with `dynamic = 'force-dynamic'` - **A/B test page** - `no-store` plus custom headers to pick the variant per request ### How revalidation works Two strategies. Time-based sets a staleness window: ```tsx const res = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } // stale after 5 minutes }) ``` Event-based tags let you invalidate on demand: ```tsx // When fetching const res = await fetch(url, { next: { tags: ['problems'] } }) // In a Server Action after a write import { revalidateTag } from 'next/cache' revalidateTag('problems') ``` `export const revalidate = 3600` at the page level is a fallback for all fetches on that page. Individual `fetch` calls can override it with their own `revalidate` value. The smallest value wins when determining when the page regenerates. ### Common mistakes **Mistake 1: `no-store` without `force-dynamic`** ```tsx // Wrong: HTML is still cached at build time export default async function Page() { const data = await fetch('/api/live', { cache: 'no-store' }) return <div>{data.value}</div> } // Fix: tell Next.js the whole route is dynamic export const dynamic = 'force-dynamic' export default async function Page() { const data = await fetch('/api/live', { cache: 'no-store' }) return <div>{data.value}</div> } ``` `fetch` with `no-store` skips the data cache. But the full route cache still snapshots the HTML at build time. You need both. **Mistake 2: `revalidate: 0` expecting it to skip everything** `revalidate: 0` only affects the data cache. Request memoization and the full route cache still apply. Use `cache: 'no-store'` with `dynamic = 'force-dynamic'` to fully opt out. **Mistake 3: stale data after mutation because of router cache** The user submits a form, a Server Action updates the database, but the next navigation shows old data. The browser's router cache served the pre-mutation RSC payload. Fix: ```tsx import { revalidatePath } from 'next/cache' export async function updateProfile(formData: FormData) { 'use server' await db.user.update(...) revalidatePath('/profile') // clears both server and router cache for this path } ``` Or call `router.refresh()` on the client after the mutation. **Mistake 4: different fetch options break memoization** Memoization keys on URL plus options plus body. Two fetches to the same URL with different `cache` values count as two separate requests. Normalize options across components that share a data source. **Mistake 5: assuming `next dev` uses the full route cache** Development mode bypasses the full route cache entirely. A page that feels static in production re-renders on every request in dev. Spent a good half-hour chasing a "caching bug" once before remembering: always verify caching behavior with `next build && next start`, not in dev. ### Real-world usage - Vercel Commerce (nextjs-commerce repo): full route cache for product detail pages, data cache with `revalidateTag` for inventory - Supabase + Next.js starters: `revalidate: 60` for user lists, `no-store` for edit pages - Next.js dashboards with NextAuth: reading `headers()` forces dynamic rendering for authenticated routes - vs React Query: Next.js caching handles server-side fetches at no extra cost; React Query fits better for client-side mutations and optimistic updates ### Follow-up questions **Q:** What is the difference between the data cache and the full route cache? **A:** Data cache stores raw fetch responses (JSON) across requests. Full route cache stores the rendered HTML and RSC payload for the whole static route at build time. **Q:** If `export const revalidate = 3600` is set at the page level, what happens to individual fetch calls with their own `revalidate`? **A:** The page-level value is a fallback. Any `fetch` with its own `next: { revalidate }` overrides it for that specific call. The smallest value determines when the page regenerates. **Q:** Hard refresh vs soft navigation: what changes? **A:** Soft navigation (clicking a `<Link>`) hits the router cache first. Hard refresh skips it entirely, goes to the server, which then checks the full route cache and data cache in that order. **Q:** Does reading `searchParams` in a page component always make the route dynamic? **A:** Yes. The moment Next.js sees `searchParams` accessed, the route opts out of the full route cache. The page re-renders per request, but the data cache still works for fetches inside it. **Q (senior):** Edge runtime vs Node.js runtime: how does caching behave differently? **A:** On Vercel, Edge runtime uses a globally distributed KV store, so cached data is shared across all regions. Node.js runtime uses the local filesystem, giving each server instance its own cache. This affects cache invalidation timing in multi-region deployments. **Q (senior):** Why does `next dev` never show the full route cache even for static pages? **A:** Development mode intentionally bypasses it so every change reflects immediately. The full route cache is only populated by `next build`. This is a common source of confusion when dev behavior does not match production. ## Examples ### Basic: request memoization with React.cache ```tsx // lib/data.ts import { cache } from 'react' import { db } from '@/lib/db' // Works for ORM queries that don't go through fetch export const getUser = cache(async (id: string) => { return db.user.findUnique({ where: { id } }) }) // app/profile/[id]/page.tsx import { getUser } from '@/lib/data' async function Avatar({ id }: { id: string }) { const user = await getUser(id) // DB call happens once return <img src={user.avatar} alt={user.name} /> } export default async function Page({ params }: { params: { id: string } }) { const user = await getUser(params.id) // reuses result, no second query return ( <div> <h1>{user.name}</h1> <Avatar id={params.id} /> </div> ) } // Output: one DB query for both components in the same render ``` `React.cache()` wraps a function so repeated calls with the same arguments return the memoized result. It resets per request, so there is no stale data across users. ### Intermediate: ISR product page with tag-based revalidation ```tsx // app/products/[slug]/page.tsx export default async function ProductPage({ params }: { params: { slug: string } }) { const res = await fetch(`https://api.example.com/products/${params.slug}`, { next: { revalidate: 3600, // regenerate at minimum every hour tags: [`product-${params.slug}`] // or on demand via revalidateTag } }) const product = await res.json() return ( <div> <h1>{product.name}</h1> <p>Stock: {product.stock}</p> <p>${product.price}</p> </div> ) } // app/actions/updateStock.ts 'use server' import { revalidateTag } from 'next/cache' export async function updateStock(slug: string, newStock: number) { await fetch(`https://api.example.com/products/${slug}`, { method: 'PATCH', body: JSON.stringify({ stock: newStock }) }) revalidateTag(`product-${slug}`) // only this product's page refreshes } ``` The page serves cached HTML until either an hour passes or `updateStock` fires `revalidateTag`. Everything else stays cached. This is the ISR pattern most ecommerce projects use. ### Advanced: dynamic route with partial data caching ```tsx // app/dashboard/page.tsx // Route is dynamic (reads cookies for auth) // but the public stats fetch is still cached import { cookies } from 'next/headers' export default async function Dashboard() { // Reading cookies makes the whole route dynamic const session = cookies().get('session-token') // This fetch still uses the data cache (5-minute window) const statsRes = await fetch('https://api.example.com/stats', { next: { revalidate: 300 } }) // This one is fresh on every request const userRes = await fetch(`/api/user/${session?.value}`, { cache: 'no-store' }) const user = await userRes.json() const stats = await statsRes.json() return ( <div> <h1>Welcome, {user.name}</h1> <p>Total users: {stats.total}</p> {/* from cache, up to 5 min stale */} </div> ) } // Output: page re-renders per request, user data is always fresh, // stats skip the API call most of the time ``` Reading `cookies()` makes the route dynamic. But individual fetches inside it still use the data cache unless you pass `no-store`. Dynamic rendering and data caching are independent. Combining them is how you get fresh user-specific data alongside cached shared data in the same render.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.