Quick answer

First check whether cacheComponents is enabled. Without it, opt data into caching with fetch or route configuration. With it, use explicit "use cache" scopes, cacheLife, and cacheTag. After a mutation, choose revalidatePath for a route, revalidateTag(tag, "max") for stale-while-revalidate, or updateTag in a Server Action for immediate read-your-own-writes behavior.

Last verified: 24 August 2026. This guide was checked against the current Next.js 16 caching and revalidation documentation.

What you'll learn

By the end of this you'll understand the two caching models available in Next.js 16, how to make a cache decision explicit, and when to reach for revalidatePath, revalidateTag, or updateTag. You'll also know how to verify behavior in a production build without trusting an outdated default.

This isn't a deep dive into every layer of the Next.js cache internals. It's the practical working knowledge you need to ship pages that stay fresh without hammering your database on every single request.

Who this is for

  • You're building with the App Router and confused about why some changes don't show up immediately after deploy
  • You've heard of ISR (Incremental Static Regeneration) but aren't sure how that concept maps to the App Router
  • You're getting stale data in production and you're not sure where the caching fits in

You can skip this if you already understand the difference between static and dynamic rendering and you just want to look up the exact revalidateTag API. The Next.js docs are faster for pure reference.

What is caching in the App Router?

Caching just means saving a computed result so you don't recalculate it on every request. Simple idea. In Next.js, it mostly means: the server built a page (or fetched some data) once, and it serves that same result to everyone until the cache expires or is manually cleared.

Plain English version: your page is like a baked loaf of bread — caching means you slice and serve that same loaf to everyone. Revalidation means you bake a new loaf.

The App Router has several caching and rendering layers, and their defaults have changed across releases. That is useful for performance, but copying an example from another version can produce the wrong freshness behavior.

Prerequisites

  • You're comfortable with the App Router (layouts, pages, Server Components)
  • You know that Server Components run on the server and can fetch data directly
  • Basic TypeScript — nothing beyond reading function signatures

If the App Router is still unfamiliar, start with the App Router basics guide. The caching model makes a lot more sense once you understand Server Components and how rendering works.

Choose your Next.js 16 caching model first

Check next.config.ts before copying an example:

  1. Without cacheComponents: true — this is still the default configuration. fetch is not cached by default. Opt individual requests into force-cache or next.revalidate, and use route-segment exports such as revalidate where appropriate.
  2. With cacheComponents: true — use "use cache", cacheLife, and cacheTag. Remove route-segment dynamic, revalidate, and fetchCache configuration as you migrate; uncached runtime work belongs behind Suspense.

The first examples below use the default model without Cache Components. A matching Next.js 16 Cache Components example follows them.

Setup from zero

We'll build a small page that shows the caching behavior you actually care about.

Step 1 — Create a page that fetches data

Make src/app/posts/page.tsx:

async function getPosts() {
  const res = await fetch(
    "https://jsonplaceholder.typicode.com/posts?_limit=5"
  );
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  return (
    <main>
      <h1>Posts</h1>
      <ul>
        {posts.map((p: { id: number; title: string }) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </main>
  );
}

By default, Next.js uses auto no cache for fetch. A route with no request-time APIs can still be prerendered and make this request once during next build, but that is not the same as explicitly placing the response in the Data Cache. If the data should be reusable, say so with cache: "force-cache" or a revalidation option.

Step 2 — Add revalidation

In a project that does not enable Cache Components, add one export above the component:

export const revalidate = 60;

This sets a 60-second route revalidation interval in the non-Cache-Components model. The exact result still depends on request-time APIs and per-fetch settings, so confirm the route mode in the production build output.

Step 3 — Verify in production mode

Run next build then next start. Check the build output — it should list the /posts route with a revalidate annotation. Reload the page repeatedly in the browser; it should feel instant because it's serving the cache. After 60 seconds, the next reload triggers the background refresh.

Little tip: development does not reproduce production route caching. The Server Component HMR cache can also reuse fetch responses — even default or no-store requests — between hot reloads; navigation or a full-page reload clears it. Use next build and next start for the real route mode and revalidation behavior.

Core idea / mental model

Without Cache Components, think in terms of route rendering plus individual data-cache choices: a route may prerender, render per request, or reuse data for a bounded time. Request-time APIs such as cookies() and headers(), uncached data, and explicit segment options influence that result.

With Cache Components enabled, the unit of caching becomes explicit: functions or UI marked "use cache" can be reused, while fresh runtime work streams through Suspense. That lets a page have a cached shell and a dynamic region instead of forcing one label onto the whole tree.

Mental model: identify the configured model, cache only the scopes that are safe to share, and attach a freshness policy to each cached scope.

Key terms

  • Static rendering — page output generated ahead of a request or cached for reuse rather than recomputed for every visitor
  • Dynamic rendering — page HTML built per request; can vary by user, cookies, headers
  • revalidate — a statically analyzable route interval used by projects without Cache Components; with Cache Components, replace it with cacheLife
  • revalidatePath — invalidates cached data for one literal path or for a route pattern when paired with the page or layout type
  • revalidateTag — marks tagged cached data stale; in Next.js 16 pass a cache-life profile such as "max" for stale-while-revalidate behavior
  • updateTag — Server Action-only invalidation for immediate read-your-own-writes behavior
  • Cache Components — the opt-in Next.js 16 model based on "use cache", cacheLife, cacheTag, and Suspense
  • fetch cache — the server-side Data Cache used only when a request opts in; plain fetch uses auto no cache by default

Step-by-step

Controlling fetch behavior per-call

Instead of a page-level revalidate, you can control caching on each individual fetch:

// Default: auto no cache
const fresh = await fetch("https://api.yoursite.com/data");

// Reuse until explicitly invalidated
const cached = await fetch("https://api.yoursite.com/data", {
  cache: "force-cache",
});

// Revalidate every 30 seconds
const timed = await fetch("https://api.yoursite.com/data", {
  next: { revalidate: 30 },
});

// Never cache — always fetch fresh
const uncached = await fetch("https://api.yoursite.com/data", {
  cache: "no-store",
});

In the model without Cache Components, an uncached request can make the route render dynamically. Do not add no-store everywhere as a debugging habit; use it when the data truly must be fetched for each request.

The Cache Components equivalent in Next.js 16

When cacheComponents: true is enabled, cache the data function explicitly:

import { cacheLife, cacheTag } from "next/cache";

export async function getPosts() {
  "use cache";
  cacheLife("minutes");
  cacheTag("posts");

  const res = await fetch("https://api.yoursite.com/posts");
  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}

Arguments passed to the cached function become part of its cache key. Leave fresh data outside a "use cache" scope and render that async region behind Suspense. Do not combine this model with copied route-segment revalidate or dynamic exports.

On-demand revalidation with revalidatePath

Time-based revalidation isn't always enough. Sometimes you want to clear the cache the moment something changes — right after a CMS publish, right after a form submission that affects the page.

import { revalidatePath } from "next/cache";

export async function publishPost(slug: string) {
  "use server";
  await db.posts.publish(slug);
  revalidatePath("/blog/" + slug);
  revalidatePath("/blog");
}

In a Server Action, these calls revalidate the affected paths and can refresh the current UI. In a Route Handler, the paths are marked for revalidation when they are next visited. In both cases, target the detail page and its listing because the mutation affects both views.

On-demand revalidation with revalidateTag

Tags let you group related fetch calls so you can invalidate them all at once:

// In your data-fetching function
const res = await fetch("https://api.yoursite.com/posts", {
  cache: "force-cache",
  next: { tags: ["posts"] },
});

// In a Server Action or Route Handler
import { revalidateTag } from "next/cache";
revalidateTag("posts", "max");

Little tip: use tags when multiple pages or layouts share the same underlying data. revalidateTag("posts", "max") marks those entries stale and serves stale content while they refresh. It works in Server Actions and Route Handlers, which makes it suitable for CMS webhooks.

When a user must see their own mutation immediately, call updateTag("posts") inside the Server Action instead. updateTag expires the entry for read-your-own-writes semantics and cannot be called from a Route Handler.

Working examples

A real blog post page with sensible caching:

import type { Metadata } from "next";

export const revalidate = 3600;

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return { title: post.title, description: post.excerpt };
}

async function getPost(slug: string) {
  const res = await fetch(
    "https://api.yoursite.com/posts/" + slug,
    { next: { revalidate: 3600, tags: ["post-" + slug, "posts"] } }
  );
  if (!res.ok) throw new Error("Post not found");
  return res.json();
}

export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.body}</div>
    </article>
  );
}

When a post is updated in the CMS, call revalidateTag("post-" + slug, "max") from a webhook Route Handler. The cached entry becomes stale and refreshes on a later visit without a redeploy.

Patterns

Static + revalidate for: projects without Cache Components where shared pages change on a schedule. Set the interval from a real freshness requirement, then verify the production route mode.

Fresh request-time data for: dashboards, authenticated pages, and anything that must differ per user or session. Without Cache Components, use cache: "no-store" where a fetch truly must be fresh. With Cache Components, leave that work outside "use cache" and place the async region behind Suspense.

Tags + revalidateTag for: CMS-driven content where stale-while-revalidate is acceptable after an editor publishes. Pair with a verified webhook from the CMS.

updateTag for: a Server Action where the same user must immediately read the value they just changed.

revalidatePath for: simpler cases where you know exactly which paths change — after a form submission that affects a specific page, for example.

Common mistakes

Setting revalidate to a variable instead of a literal

// Unsupported: the value is not statically analyzable
const REVALIDATE_SECONDS = 60;
export const revalidate = REVALIDATE_SECONDS;

// This works
export const revalidate = 60;

The non-Cache-Components model needs to read revalidate statically during build analysis. Use a plain number literal rather than a variable or expression. If Cache Components is enabled, remove this segment export and use cacheLife in the cached scope.

Treating next dev as a production cache test

Development renders pages on demand, but the Server Component HMR cache can still reuse fetch responses between hot reloads, including no-store requests. Navigation or a full reload clears that HMR cache, and a hard refresh may send cache-control: no-cache. Use next build && next start to verify production behavior.

One cache: "no-store" fetch pulling the whole page into dynamic rendering

Without Cache Components, an uncached fetch can make the route dynamic. With Cache Components, keep the live async region outside "use cache" and wrap it in Suspense so cached and uncached work can coexist. Choose the pattern that matches the project's config instead of applying one rule to both models.

Wrong path format in revalidatePath

Use a literal URL such as revalidatePath("/blog/my-post-title") to invalidate one page. To invalidate every page matching a dynamic route, a pattern is supported when you pass its type: revalidatePath("/blog/[slug]", "page"). Paths are case-sensitive.

Troubleshooting

Page isn't updating after setting revalidate: First confirm the project does not enable Cache Components; that model uses cacheLife instead. Then run next build && next start, inspect the route mode, and confirm a request occurred after the revalidation window.

revalidateTag not working: Check that the cached source actually carries the same tag — through next.tags or cacheTag — and call revalidateTag("your-tag", "max") for stale-while-revalidate behavior. If a Server Action must show the mutation immediately, use updateTag instead.

Build error about revalidate: You've assigned a variable to it. Change it to a number literal.

Page rendering dynamically despite revalidate: Without Cache Components, inspect the route for request-time APIs such as cookies() or headers() and for uncached data. With Cache Components, remove the segment revalidate export and move reusable work into a "use cache" scope with cacheLife.

Checklist

  • [ ] Checked next.config.ts and chose the correct model for cacheComponents
  • [ ] Without Cache Components, export const revalidate = 60 uses a plain number literal (not a variable)
  • [ ] Caching tested with next build && next start, not next dev
  • [ ] Build output confirms routes are static or ISR as intended
  • [ ] Fresh work uses cache: "no-store" without Cache Components, or stays outside "use cache" with Cache Components
  • [ ] On-demand revalidation (if used) tested through its Server Action or Route Handler
  • [ ] Without Cache Components, no accidental cookies(), headers(), or uncached data makes a route dynamic
  • [ ] Tags are consistent between the fetch calls and the revalidateTag calls (exact string match)
  • [ ] Next.js 16 revalidateTag calls include a cache-life profile such as "max"
  • [ ] updateTag is used only in Server Actions that need immediate read-your-own-writes behavior

Practice task

In a project without Cache Components, take a shared page that fetches data and add export const revalidate = 300. Run next build, inspect its route mode, and explain every request-time API or uncached fetch that affects the result. If Cache Components is enabled, perform the same exercise with a "use cache" data function and cacheLife("minutes") instead.

FAQ

What's the difference between revalidate in the fetch options vs. the route segment export?
Without Cache Components, the route segment export const revalidate = 60 sets a default interval while { next: { revalidate: 30 } } sets one for that fetch; the lower interval affects the route. With Cache Components, use cacheLife instead of the segment export.

Does revalidatePath work inside a Server Action?
Yes. After the mutation, a Server Action can call revalidatePath so affected data is revalidated and the current UI can refresh. A Route Handler can also call it, but that marks the path for revalidation on its next visit.

What do users see while a background rebuild is happening?
For a stale-while-revalidate policy such as revalidateTag(tag, "max"), the first request can receive the stale cached value while a fresh one is produced in the background. Other invalidation methods have different timing, so test the exact path rather than assuming every API behaves this way.

Can I turn off all caching globally for debugging?
Without Cache Components, export const dynamic = "force-dynamic" on a layout forces request-time rendering below it, but it is a broad diagnostic lever rather than a production fix. With Cache Components, remove that segment option and keep intentionally fresh work outside cached scopes.

What to learn next

Once caching is working correctly, connect the HTTP side with the Next.js Route Handlers guide, then measure images and fonts with the Lighthouse performance cheat sheet. For CMS content, authenticate the webhook and call revalidateTag(tag, "max") so tagged data refreshes without a redeploy.

Official Next.js sources checked

Takeaways

Next.js 16 has two caching models, so the configuration comes first. Without Cache Components, opt requests into caching and use statically analyzable route revalidation where it fits. With Cache Components, cache explicit data or UI scopes with "use cache", give them a cacheLife, and stream uncached work through Suspense.

If you remember only one thing: do not copy caching syntax before checking cacheComponents. The correct API is either route/fetch revalidation or "use cache" with cacheLife — mixing those models is how otherwise careful code ends up stale or unsupported.