9. Caching — The Most Important Topic
Caching is where most customer problems originate. Understanding every cache layer is essential.
9.1 The four cache layers
Layer 1: Browser Cache (client-side, user's browser)
Layer 2: Edge Cache (Vercel CDN, global PoPs)
Layer 3: Data Cache (Next.js fetch() cache, per-function)
Layer 4: Full Route Cache (rendered HTML cache, stored on CDN)
9.2 Next.js fetch() cache
// DEFAULT: cached indefinitely (force-cache)
fetch('https://api.example.com/data')
// Cache for 60 seconds, then revalidate
fetch('https://api.example.com/data', { next: { revalidate: 60 } })
// Never cache (always fresh)
fetch('https://api.example.com/data', { cache: 'no-store' })
// Tag-based: revalidate all fetches with this tag
fetch('https://api.example.com/products', { next: { tags: ['products'] } })
9.3 On-demand revalidation
import { revalidateTag, revalidatePath } from 'next/cache';
// When a product is updated in your CMS, call this API route:
export async function POST(request) {
const { productId } = await request.json();
revalidateTag('products'); // revalidate all tagged fetches
revalidatePath(`/products/${productId}`); // revalidate specific page
return Response.json({ revalidated: true });
}
This is the webhook pattern — your CMS (Contentful, Sanity, etc.) triggers a webhook on publish, which calls your Next.js API route, which revalidates the relevant cached pages. This is how ISR + headless CMS works in production.
9.4 unstable_cache (for non-fetch data)
For database queries and other data sources that don't use fetch:
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/database';
const getCachedProducts = unstable_cache(
async () => {
return db.query('SELECT * FROM products');
},
['products'], // cache key
{
revalidate: 3600, // 1 hour
tags: ['products'] // revalidate with revalidateTag('products')
}
);
9.5 Common caching anti-patterns (for code audits)
| Anti-pattern | Problem | Fix |
|---|---|---|
fetch() in a Client Component | Bypasses server-side cache, exposes API keys | Move to Server Component |
cache: 'no-store' on every fetch | Every request hits origin — kills performance, costs money | Add appropriate revalidate |
| No cache tags | Can't do targeted on-demand revalidation | Add tags to related fetches |
| ISR without webhooks | Content updates not visible until revalidate period expires | Add CMS webhook → revalidatePath |
| Caching user-specific data | User A sees User B's data | Add user ID to cache key or disable cache |
| Caching in Edge Middleware | Edge middleware runs on every request — should not cache | Move caching to Serverless layer |