Time is a poor trigger for content that changes when somebody edits it. Tag such entries instead, give them a long lifetime, and invalidate them from the code that did the editing. cacheTag attaches labels to the entry being produced:
export async function getNotes() {
"use cache";
cacheLife("max");
cacheTag("notes");
return { notes: listNotes(), at: new Date().toISOString() };
}The Server Action that mutates the data clears the label afterwards:
"use server";
import { updateTag } from "next/cache";
import { addNote } from "./store";
export async function createNote(formData) {
addNote(String(formData.get("text") || "Untitled"));
updateTag("notes");
}The page reads getNotes() and renders a form whose action is createNote. Reading twice, submitting, then reading twice more:
before : last at 2026-09-22T05:57:55.295Z | Ship the release notes before : last at 2026-09-22T05:57:55.295Z | Ship the release notes after : last at 2026-09-22T05:58:19.996Z | Ship the release notes | Draft the changelog after : last at 2026-09-22T05:58:19.996Z | Ship the release notes | Draft the changelog
cacheLife("max") means a 30-day revalidate, so nothing but the tag could have moved that timestamp.
Three functions invalidate, and the choice matters:
| Function | Where it runs | Behavior |
|---|---|---|
| updateTag(tag) | Server Actions only | Expires immediately; the next read waits for fresh data |
| revalidateTag(tag, profile) | Actions and Route Handlers | Serves the stale entry for profile while regenerating |
| revalidatePath(path) | Actions and Route Handlers | Same, for every entry a route touches |
Use updateTag after a mutation the same user is about to see: serving them a list without the note they just added would look broken. Use revalidateTag for a CMS webhook or an admin endpoint, where a few seconds of stale content costs nothing; pass 'max' as the second argument for the longest stale window. Tags are case-sensitive and capped at 256 characters. refresh() from next/cache is also Server-Action-only, but it refreshes the client router rather than expiring anything on the server.
Prefer tags over paths. A path invalidates everything a route rendered, including entries that were still correct; a tag names the data that changed, and one tag can cover several functions at once.