DEV Community

Cover image for Next.js 16 Caching Explained: Revalidation, Tags, Draft Mode & Real Production Patterns
RealACJoshua
RealACJoshua

Posted on

Next.js 16 Caching Explained: Revalidation, Tags, Draft Mode & Real Production Patterns

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");
Enter fullscreen mode Exit fullscreen mode

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 }
});
Enter fullscreen mode Exit fullscreen mode

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"
});
Enter fullscreen mode Exit fullscreen mode

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"] }
});
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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"]
  }
});
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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)