LearnFrenzy
Sign in Take a free test

Caching and Revalidation · Lesson 13 of 24

Revalidating Data: updateTag, revalidateTag, revalidatePath

6 min read Updated Nov 8, 2026 Saurabh Samir

Cached data has to be refreshed when it changes – a new blog post is published, a price is edited, a user updates their profile. Time-based revalidation (cacheLife) handles data that may be a little out of date. For everything else there's on-demand revalidation: label cached data with cacheTag, and expire that label right after the change.

In this lesson

  • Tagging cached data with cacheTag
  • updateTag – show the change immediately (shown live)
  • revalidateTag – refresh in the background
  • revalidatePath and refresh

Tag, change, update

A cached list of announcements, and two ways to publish a new one. The form calls a Server Action – a function marked "use server" that runs on the server when the form is submitted (the next chapter covers them fully):

// lib/announcements.ts
import { cacheLife, cacheTag } from "next/cache";

const announcements = ["Next.js course launched"];

export async function getAnnouncements() {
  "use cache";
  cacheLife("max");                              // keep it until we say otherwise
  cacheTag("announcements");
  return [...announcements];
}

export async function saveAnnouncement(text: string) {
  announcements.push(text);                      // e.g. INSERT INTO announcements …
}
// app/actions.ts
"use server";

import { updateTag } from "next/cache";
import { saveAnnouncement } from "@/lib/announcements";

export async function publishQuietly(formData: FormData) {
  await saveAnnouncement(String(formData.get("text")));
  // forgot to revalidate!
}

export async function publish(formData: FormData) {
  await saveAnnouncement(String(formData.get("text")));
  updateTag("announcements");                    // expire the cache now; this user sees the change
}
// app/page.tsx
import { getAnnouncements } from "@/lib/announcements";
import { publish, publishQuietly } from "./actions";

export default async function Home() {
  const items = await getAnnouncements();
  return (
    <main>
      <h1>Announcements</h1>
      <ul>{items.map((a) => <li key={a}>{a}</li>)}</ul>
      <form action={publishQuietly}>
        <input name="text" aria-label="Quiet text" defaultValue="TypeScript course updated" />
        <button>Publish without revalidating</button>
      </form>
      <form action={publish}>
        <input name="text" aria-label="Text" defaultValue="React 19 lessons added" />
        <button>Publish and update tag</button>
      </form>
    </main>
  );
}

Visiting the page

Announcements
• Next.js course launched
[TypeScript course updated] [Publish without revalidating]
[React 19 lessons added] [Publish and update tag]

› click “Publish without revalidating”
Announcements
• Next.js course launched
[TypeScript course updated] [Publish without revalidating]
[React 19 lessons added] [Publish and update tag]

› reload the page
Announcements
• Next.js course launched
[TypeScript course updated] [Publish without revalidating]
[React 19 lessons added] [Publish and update tag]

› click “Publish and update tag”
Announcements
• Next.js course launched
• TypeScript course updated
• React 19 lessons added
[TypeScript course updated] [Publish without revalidating]
[React 19 lessons added] [Publish and update tag]

The first announcement was saved, but the page kept showing the cached list – even after a reload – because nothing told the cache. The second action called updateTag("announcements"): the cache entry was expired, the list was read again from the data store (now containing both new items), and the updated page came back in the same round trip as the action.

updateTag vs revalidateTag

updateTag(tag)revalidateTag(tag, "max")
WhereServer Actions onlyServer Actions and Route Handlers
Behaviourexpires immediately; the next read waits for fresh datamarks stale; serves the old data while fresh data loads in the background
Use for"read your own writes" – the user who made the change must see itcontent updates where a short delay is fine – a CMS webhook, a nightly import
// app/api/cms-webhook/route.ts – called by your CMS when an editor publishes
import { revalidateTag } from "next/cache";

export async function POST(request: Request) {
  if (request.headers.get("x-webhook-secret") !== process.env.CMS_WEBHOOK_SECRET) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
  revalidateTag("posts", "max");                // stale-while-revalidate
  return Response.json({ revalidated: true });
}

The second argument of revalidateTag is a cacheLife profile that says how long stale content may still be served while the refresh happens; "max" is the recommended value.

revalidatePath and refresh

"use server";

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

export async function updateProfile(formData: FormData) {
  await db.user.update({ where: { id: 1 }, data: { name: String(formData.get("name")) } });
  revalidatePath("/profile");                   // everything cached for this route
}

export async function markNotificationsRead() {
  await db.notification.updateMany({ data: { read: true } });
  refresh();                                    // re-render the current page's uncached data
}
  • revalidatePath(path) – invalidates all cached data for a route when you don't know (or don't want to list) its tags. Tags are more precise, so prefer them.
  • refresh() – in a Server Action, re-renders the current page so uncached data (like a notification count) updates, without touching the cache.

A simple tagging strategy

Tag by entity type and by item: cacheTag("posts", `post-${id}`). Editing one post calls updateTag(`post-${id}`); publishing a new post calls updateTag("posts") so every list refreshes.

Practice

Try it yourself

  1. Cache a "course of the day" with tag featured, and add a button whose action changes it and calls updateTag.

    Show solution
    // lib/featured.ts
    import { cacheLife, cacheTag } from "next/cache";
    
    let featured = "React.js";
    
    export async function getFeatured() {
      "use cache";
      cacheLife("days");
      cacheTag("featured");
      return featured;
    }
    
    export async function setFeatured(title: string) {
      featured = title;
    }
    // app/page.tsx
    import { updateTag } from "next/cache";
    import { getFeatured, setFeatured } from "@/lib/featured";
    
    async function feature(formData: FormData) {
      "use server";
      await setFeatured(String(formData.get("title")));
      updateTag("featured");
    }
    
    export default async function Home() {
      return (
        <main>
          <p>Course of the day: {await getFeatured()}</p>
          <form action={feature}>
            <input type="hidden" name="title" value="Next.js" />
            <button>Feature Next.js</button>
          </form>
        </main>
      );
    }

    Page

    Course of the day: React.js
    [Feature Next.js]
    
    › click “Feature Next.js”
    Course of the day: Next.js
    [Feature Next.js]

Quick check

Which function shows the user their own change immediately?

updateTag, from a Server Action.

Which one can a Route Handler (e.g. a CMS webhook) call?

revalidateTag(tag, "max").

What happens if you save data but don't revalidate a "max" cache?

Visitors keep seeing the old cached result.

Summary

  • Label cached data with cacheTag; invalidate it after every change.
  • updateTag for immediate read-your-own-writes in Server Actions; revalidateTag(tag, "max") for background refreshes.
  • revalidatePath invalidates a whole route; refresh() re-renders uncached data on the current page.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular