Caching and Revalidation · Lesson 13 of 24
Revalidating Data: updateTag, revalidateTag, revalidatePath
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 backgroundrevalidatePathandrefresh
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>
);
}
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") | |
|---|---|---|
| Where | Server Actions only | Server Actions and Route Handlers |
| Behaviour | expires immediately; the next read waits for fresh data | marks 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 it | content 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
Cache a "course of the day" with tag
featured, and add a button whose action changes it and callsupdateTag.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> ); }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.