Skip to content
CodeAndBuild LogoCodeAndBuild

Next.js

How to Cache and Revalidate Data in the Next.js App Router

Tag fetches, choose a lifetime, and revalidate one slice of a Next.js App Router site when content changes.

CodeAndBuild Team8 min read
  • Next.js
  • Caching
  • App Router
  • Performance
On this page
  1. Three layers, three decisions
  2. Tag the read
  3. Invalidate from the write
  4. Mistakes that look like framework bugs
  5. Choose the smallest fresh-enough window

A Next.js App Router page can be fast and still be wrong. The framework is allowed to reuse a rendered result, a fetch response, and a full route until you tell it that a particular slice of data changed. Teams that treat caching as an on/off switch either ship a slow site or ship a site that keeps yesterday's headline. The useful model is smaller: every expensive read gets a tag, every tag has an owner, and a write revalidates only the tags it actually changed.

This guide uses the patterns that are stable on Next.js 15 and Next.js 16: fetch options with a tag, revalidateTag and revalidatePath from a Server Action, and a deliberate choice between time-based and on-demand freshness. The examples are for a tech blog or a small product dashboard. They are not a cache for authenticated user data. Personalized pages should stay dynamic.

Three layers, three decisions

The first layer is the data read. A fetch to your own CMS, or a call you wrap so it can be tagged, is the unit you will invalidate later. The second layer is the route. A page at /blog/my-post is HTML that included that data at render time. The third layer is the client router cache, which can keep a previously visited page on the user's machine for a short time even after the server has a new version. If you only clear one layer, the bug report will say the editor saved the post and the public page did not move.

LayerWhat it storesHow you refresh it
Tagged fetchThe JSON or row you readrevalidateTag("posts")
Full routeThe rendered page for one URLrevalidatePath("/blog/my-post")
Time-basedAnything older than N secondsrevalidate: 3600 on the fetch or the page

Tag the read

Give shared content a stable tag and give a single record a second, more specific tag. The list page and the sitemap can share posts. The article page can also carry post:slug so a title change does not have to rebuild every guide. Tags are strings you invent. They are only useful if the write path uses the same strings the read path used.

lib/posts-api.tsts
export async function getPublishedPosts() {
  const response = await fetch("https://cms.example.com/posts", {
    next: { tags: ["posts"], revalidate: 3600 },
  });

  if (!response.ok) {
    throw new Error("Post list failed");
  }

  return response.json();
}

The revalidate value is a backstop, not the product requirement. Sixty minutes means a failed webhook cannot leave the site stale forever. The tag is how you get a publish onto the site in seconds. If the CMS is down, throw. Caching an error response is how a five-minute outage becomes a day of empty pages.

Invalidate from the write

Revalidation belongs next to the mutation. A Server Action that updates a post should revalidate the list tag and the path of that post after the database write commits. Doing it in a useEffect on the client races the navigation and can run twice. Doing it in a cron that revalidates everything hides which write was responsible.

app/editor/actions.tsts
"use server";

import { revalidatePath, revalidateTag } from "next/cache";

export async function publishPost(slug: string) {
  await savePublishedPost(slug);
  revalidateTag("posts");
  revalidatePath("/blog/" + slug);
  revalidatePath("/");
}

declare function savePublishedPost(slug: string): Promise<void>;
  1. 01

    Name tags before you write the page

    Write down the tags a reader-facing page depends on. posts for the index, post:slug for one article, and nothing for a page that is unique per user.

  2. 02

    Put the same tags on the read

    Pass them in next.tags. Add a revalidate number so a missed webhook still expires.

  3. 03

    Revalidate after the commit

    Call revalidateTag and revalidatePath only after the save succeeds. A failed save must not wipe a good cache.

  4. 04

    Check both URLs

    Open the article and the homepage in a private window. The editor's own browser is the worst place to judge freshness because of the client router cache.

Mistakes that look like framework bugs

  • Tagging a fetch and then calling revalidatePath only. The page can render again with the old fetch payload still in the data cache.
  • Using a tag that includes a timestamp. Nothing you write later will match it, so the cache never clears.
  • Caching a request that sends a cookie or an Authorization header. You will leak one user's data into another user's page, or you will disable the cache without noticing.
  • Revalidating / and forgetting /sitemap.xml. Search engines will keep the old lastmod until that route expires on its own.

Authenticated dashboards should opt out. Export a dynamic segment, read cookies inside the render, or fetch with cache: "no-store" for that one call. The rest of the marketing site can stay static. Mixing the two in one fetch helper is how a billing page gets cached under the first user who opened it.

Choose the smallest fresh-enough window

A changelog can wait an hour. A status page cannot. A blog post should update when an editor presses publish, and it should also expire overnight in case the webhook was lost. Write that requirement next to the tag. When someone later asks why the homepage is slow, you can point at the one untagged fetch instead of turning caching off for the whole app.

More guides