Next.js 16 Caching Explained: Revalidation, Tags, Draft Mode & Real Production Patterns
Source: Dev.to
🎯 What We’re Building
A production‑ready mental model for caching:
- Static + dynamic control using
fetch - Tag‑based invalidation
- On‑demand revalidation
- Draft mode for preview workflows
- Real‑world patterns I actually use
No guesswork. No accidental stale pages.
🧠 First: The New Mental Model
In Next.js 16, caching is no longer “page‑based”.
It’s data‑based. The unit of caching is now the fetch() call.
- Every
fetchcan be cached or dynamic. - Every
fetchcan define revalidation rules. - Every
fetchcan be invalidated via tags.
This is cleaner and more scalable.
🔥 1. Controlling Cache with fetch
Default behavior
const res = await fetch("https://api.example.com/posts");
By default, this response is cached in production.
Static with Revalidation (ISR‑style)
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 }
});
- Cache this response.
- Revalidate every 60 seconds.
Replaces older ISR patterns with finer granularity.
Fully Dynamic (No Cache)
const res = await fetch("https://api.example.com/posts", {
cache: "no-store"
});
Forces dynamic rendering. Use when:
- User‑specific data
- Authenticated dashboards
- Rapidly changing metrics
🏷️ 2. The Real Upgrade: Cache Tags
Next.js 16 lets you tag cached fetches:
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] }
});
The cache entry is now associated with the "posts" tag, enabling manual invalidation.
🚀 3. On‑Demand Revalidation with Tags
Create a route handler that revalidates a tag:
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST() {
revalidateTag("posts");
return Response.json({ revalidated: true });
}
Calling this endpoint instantly invalidates all cached fetches tagged "posts"—precise, not page‑level, not global.
🧪 4. Combining Revalidation + Tags (Best Pattern)
const res = await fetch("https://api.example.com/posts", {
next: {
revalidate: 3600,
tags: ["posts"]
}
});
- Automatic hourly refresh
- Manual invalidation when needed
- No unnecessary rebuilds
📝 5. Draft Mode for Preview Workflows
Enabling draft mode
import { draftMode } from "next/headers";
export async function GET() {
draftMode().enable();
return Response.redirect("/admin");
}
Using draft mode in a page
import { draftMode } from "next/headers";
export default async function Page() {
const { isEnabled } = draftMode();
const res = await fetch("https://api.example.com/posts", {
cache: isEnabled ? "no-store" : "force-cache"
});
const data = await res.json();
return { data.title };
}
When draft mode is active:
- Cache is bypassed
- Unpublished changes are visible
When off, normal caching resumes.
⚙️ 6. Production Pattern I Actually Use
| Use case | Cache config |
|---|---|
| Public content | next: { revalidate: 600, tags: ["posts"] } |
| Admin updates | revalidateTag("posts") |
| User dashboards | cache: "no-store" |
| Preview routes | draftMode + no-store |
Result: performance, freshness, precision, scalability.
⚠️ Common Mistakes I Made
- Mixing
cache: "no-store"withrevalidate - Forgetting tags and trying to revalidate entire paths
- Assuming dev mode reflects production caching
- Over‑invalidating
Tip: Dev mode behaves differently. Always test caching in a production build:
next build
next start
🧩 How This Changes Everything
Before Next.js 16, caching felt page‑based and indirect.
Now it’s:
- Declarative
- Granular
- Fully controllable
The shift from page ISR to fetch‑level caching is a major architectural improvement.
🏁 Final Thoughts
Next.js 16 doesn’t just improve caching—it makes it predictable.
If you understand:
fetchcache controlrevalidatetagsrevalidateTag()draftMode()
you control performance instead of guessing it.
If this clarified things for you, feel free to share it with other frontend engineers battling stale data.
I’d love to see any interesting caching patterns you’ve built in Next.js 16.
More deep dives coming.
Check me out at .