Caching & Revalidation Strategies
Next.js has a sophisticated caching system. Understanding it is crucial for building fast, fresh applications.
The Caching Layers
Next.js caches at multiple levels:
| Layer | What | Where | When to Use |
|---|---|---|---|
| Request Memoization | Fetch deduplication | Server | Automatic for same-render fetches |
| Data Cache | Fetch results | Server | Persistent data between requests |
| Full Route Cache | Rendered pages | Server | Static pages |
| Router Cache | Prefetched routes | Client | Navigation optimization |
Fetch Caching Options
Control caching with fetch options:
// Cached indefinitely (default for static pages)
fetch('https://api.example.com/data')
// Revalidate every 60 seconds
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// Never cache (always fresh)
fetch('https://api.example.com/data', {
cache: 'no-store'
})
// Cache with a tag for targeted revalidation
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
Time-Based Revalidation
Set revalidation at the fetch, page, or layout level:
// Per-fetch revalidation
const data = await fetch(url, { next: { revalidate: 3600 } })
// Page-level revalidation
export const revalidate = 3600 // Revalidate every hour
export default async function Page() {
// All fetches inherit this revalidation time
}
On-Demand Revalidation
Revalidate when data changes:
// By path
import { revalidatePath } from 'next/cache'
export async function createPost() {
await db.posts.create({ ... })
revalidatePath('/posts') // Revalidate a specific page
revalidatePath('/posts', 'layout') // Revalidate all pages under /posts
}
// By tag
import { revalidateTag } from 'next/cache'
// When fetching, add a tag
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// When mutating, revalidate the tag
export async function createPost() {
await db.posts.create({ ... })
revalidateTag('posts')
}
Opting Out of Caching
Force dynamic behavior:
// Page-level
export const dynamic = 'force-dynamic'
// Or use dynamic functions
import { cookies, headers } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies() // Makes page dynamic
const headersList = await headers() // Makes page dynamic
}
Caching Gotchas
Gotcha 1: POST requests aren't cached
// This is always fresh
fetch(url, { method: 'POST' })
Gotcha 2: Dynamic functions opt out of caching
Using cookies(), headers(), or searchParams makes the route dynamic.
Gotcha 3: Route Handlers cache differently
// GET is cached by default
export async function GET() { }
// Use dynamic to opt out
export const dynamic = 'force-dynamic'
The Mental Model
Think of caching as layers of a cake:
- Build time: Pages without dynamic data are pre-rendered
- Request time: Dynamic pages render on each request
- Revalidation: Stale pages update in the background
// Static (cached at build)
export default async function Page() {
const data = await fetch('https://api.example.com/static-data')
return <div>{data}</div>
}
// Dynamic (fresh each request)
export default async function Page() {
const user = await cookies() // Dynamic function
return <div>Hello {user}</div>
}
// ISR (cached, revalidates periodically)
export const revalidate = 60
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>{data}</div>
}
Summary
- Next.js caches at multiple layers automatically
- Use
next: { revalidate: seconds }for time-based updates - Use
next: { tags: ['name'] }for targeted revalidation - Call
revalidatePathorrevalidateTagafter mutations - Dynamic functions (
cookies,headers) opt out of caching - Use
dynamic = 'force-dynamic'to disable caching entirely

