I used to treat caching in Next.js like a superstition.
Sometimes revalidate worked.
Sometimes ISR felt magical.
Sometimes nothing updated and I questioned reality.
Then Next.js 16 dropped — and the caching model finally made sense.
If you’re building production apps and want predictable caching, controlled invalidation, and proper preview workflows, this is the breakdown I wish I had earlier.
In this post, I’ll show you exactly how I now structure caching in Next.js 16 — using tags, on-demand revalidation, improved fetch controls, and draft mode.
🎯 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.
That means:
• Every fetch can be cached or dynamic
• Every fetch can define revalidation rules
• Every fetch can be invalidated via tags
This is cleaner and more scalable.
🔥 1. Controlling Cache with fetch
Here’s the default behavior:
const res = await fetch("https://api.example.com/posts");
By default, this is cached in production.
Now let’s control it explicitly.
Static with Revalidation (ISR-style)
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 }
});
This means:
• Cache this response
• Revalidate every 60 seconds
This replaces older ISR patterns in a more granular way.
Fully Dynamic (No Cache)
const res = await fetch("https://api.example.com/posts", {
cache: "no-store"
});
This forces dynamic rendering.
Use this when:
• User-specific data
• Authenticated dashboards
• Rapidly changing metrics
🏷️ 2. The Real Upgrade: Cache Tags
This is where Next.js 16 becomes powerful.
You can now tag cached fetches.
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] }
});
Now the cache is associated with the "posts" tag.
Why does this matter?
Because you can invalidate it manually.
🚀 3. On-Demand Revalidation with Tags
Let’s say you create a new blog post via an admin panel.
You don’t want to wait 60 seconds.
You want instant refresh.
Create a route handler:
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST() {
revalidateTag("posts");
return Response.json({ revalidated: true });
}
Now whenever you hit this endpoint, all cached fetches tagged "posts" are invalidated.
This is precise.
Not page-level.
Not global.
Targeted.
This is production-grade control.
🧪 4. Combining Revalidation + Tags (Best Pattern)
This is what I now use in real projects:
const res = await fetch("https://api.example.com/posts", {
next: {
revalidate: 3600,
tags: ["posts"]
}
});
What this gives me:
• Automatic hourly refresh
• Manual invalidation when needed
• No unnecessary rebuilds
This is the sweet spot.
📝 5. Draft Mode for Preview Workflows
If you’re building a CMS-driven app, preview matters.
Next.js 16 improves draft handling significantly.
Enable draft mode:
import { draftMode } from "next/headers";
export async function GET() {
draftMode().enable();
return Response.redirect("/admin");
}
Then inside your 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 <div>{data.title}</div>;
}
When draft mode is active:
• Cache is bypassed
• You see unpublished changes
When off:
• Full caching resumes
This makes preview systems predictable.
⚙️ 6. Production Pattern I Actually Use
Here’s my standard architecture:
Public content:
next: { revalidate: 600, tags: ["posts"] }
Admin updates:
revalidateTag("posts")
User dashboards:
cache: "no-store"
Preview routes:
draftMode + no-store
This gives:
• Performance
• Freshness
• Precision
• Scalability
⚠️ Common Mistakes I Made
• Mixing cache: "no-store" with revalidate
• Forgetting tags and trying to revalidate entire paths
• Assuming dev mode reflects production caching
• Over-invalidating
Remember: Dev mode behaves differently.
Always test caching behavior in production builds:
next build
next start
🧩 How This Changes Everything
Before Next.js 16:
Caching felt page-based and indirect.
Now:
It’s declarative.
It’s granular.
It’s 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:
• fetch cache control
• revalidate
• tags
• revalidateTag()
• draftMode()
You control performance instead of guessing it.
If this clarified things for you, drop a ❤️ or share it with another frontend engineer fighting stale data.
And if you’ve built an interesting caching pattern in Next.js 16, I’d like to see it.
More deep dives coming.
Check me out at https://theacj.com.ng
Top comments (0)