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 .