We've been covering on-demand revalidation. Three functions allow you to revalidate on demand:
revalidatePathrevalidateTagupdateTag
In the previous chapter, we took a look at revalidatePath. In this chapter we cover the other two:
revalidateTagallows you to invalidate cached data on-demand for a specific cache tag.
updateTagallows you to update cached data on-demand for a specific cache tag from within Server Actions.
We target cached data, that is data cache entries.
Create a tag
A cache tag (string[]) is a tag you add to a data fetch:
fetch(url, { next: { tags: ['posts', 'post-a'] } });
Note: there is a second option to create a tag: cacheTag. We will cover this in a later chapter.
Tagging a fetch will add the data to the data cache with those tag(s). You can then call revalidateTag or updateTag to revalidate or update the data.
updateTag
updateTag(tag: string): void;
- Takes one argument: a single tag.
- Can only be called from
server actions. - Revalidates data as
read-your-own-writes.
This should sound familiar. A function called from a server action performs a read-your-own-writes revalidation. Note that updateTag cannot be called from a route handler.
revalidateTag
revalidateTag(tag: string, profile: string | { expire?: number }): void;
- First argument: a single tag.
- Second argument (profile): 'max' or { expire: 0 }
- Use it in
route handlers. - Don't use it in
server actions(you can, but useupdateTaginstead).
revalidateTag is a bit weird because its functionality seems to overlap with updateTag and that's true. updateTag is a recent addition to Next and it takes over some of the functionality of revalidateTag. However, for legacy support, revalidateTag keeps its original functionality. The rule is simple:
-
route handlers->revalidateTag -
server actions->updateTag
It gets a bit weirder with the second argument of revalidateTag:
// inside route handler
revalidateTag('post-a', 'max'); // -> stale while revalidate
revalidateTag('post-a', { expire: 0 }); // -> read your own writes
So, when called from within a route handler, revalidateTag can revalidate via SWR or RYOW. revalidatePath can only do stale while revalidate.
Full route cache
revalidateTag and updateTag target the data cache, tagged data cache entries. But what happens with the full route cache - statically rendered routes? The short answer is as expected.
revalidateTag(tag, "max")(swr) in a static route
When calling revalidateTag(tag, "max") from a route handler, Next will mark the tagged data cache entry as stale. Any statically rendered routes that "depend" on this cache tag, will also be marked as stale. Note: no updates have been made at this point.
When Next receives a new request for a route that depends on the invalidated cache tag, it will serve the stale content and update the data cache and full route cache in the background. Only the next request will be served this now-fresh content. This is pure SWR. We've covered this. Let's write a test.
Note: the examples are available on github. We make a new route handler that will call revalidateTag. As before, we will hit this route handler directly to simulate an external call:
localhost:3000/revalidate/api/revalidateTag?tag=post-42&profile=max
This time we pass 2 searchParams: a tag and a profile value. This is our route handler:
// app/revalidate/api/revalidateTag/route.ts
import type { NextRequest } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function GET(request: NextRequest) {
const tag = request.nextUrl.searchParams.get('tag');
const profileParam = request.nextUrl.searchParams.get('profile');
const profile: 'max' | { expire: 0 } = profileParam
? profileParam === 'expire'
? { expire: 0 }
: 'max'
: 'max';
if (tag) {
revalidateTag(tag, profile);
return Response.json({ revalidated: true });
}
return Response.json({
revalidated: false,
message: 'Missing tag to revalidate',
});
}
We also create a route for some post components:
// app/revalidate/tag/max/page.tsx
export default function Page() {
return (
<>
<Post postId={41} fetchOptions={{}} />
<Post
postId={42}
fetchOptions={{ next: { revalidate: 1800, tags: ['post-42'] } }}
/>
<Post postId={43} fetchOptions={{ cache: 'force-cache' }} />
</>
);
}
And we run a test:
[action]
next build
next start
visit localhost:3000/revalidate/api/revalidateTag?tag=post-42&profile=max
get revalidated response
visit /revalidate/tag/max
[expect]
✅ pass: all time stamps to be equal (from build)
[action]
refresh page
[expect]
✅ pass: post 41 to have updated time stamp
✅ pass: post 42 to have updated time stamp
✅ pass: post 43 not to have changed
We revalidated post 42. We were served stale content on the first request. It updated in the background. On a new request (refresh), it served fresh data. Post 43 did not update. This is as expected. Post 41 also updated. This may or may not surprise you. Post 41 is a fetch with the auto cache option, which means no cache. When we revalidated post-42, it triggered a data cache and full route cache update. The last one made a new fetch for post 41 and that is why it updated.
We proved that revalidateTag only revalidates the tagged cache and behaves SWR when called from a route handler with the argument 'max'.
revalidateTag(tag, {expire: 0})(RYOW) in a static route
Here is a new example. We're calling revalidateTag with arguments 'post-45' and { expire: 0 } from a router handler. We expect read your own writes. We reuse the same route handler from the previous example but update the searchParams to tag "post-45" and profile "expire":
localhost:3000/revalidate/api/revalidateTag?tag=post-45&profile=expire
And we create a new test route:
// app/revalidate/tag/expire/page.tsx
import Post from '@/components/Post';
export default function Page() {
return (
<>
<Post postId={44} fetchOptions={{}} />
<Post
postId={45}
fetchOptions={{ next: { revalidate: 1800, tags: ['post-45'] } }}
/>
<Post postId={46} fetchOptions={{ cache: 'force-cache' }} />
</>
);
}
[action]
next build
next start
visit localhost:3000/revalidate/api/revalidateTag?tag=post-45&profile=expire
get revalidated response
visit /revalidate/tag/expire
[expect]
✅ pass: post 44 and post 45 to have same time stamp (updated)
✅ pass: post 46 to have different time stamp (from build)
My test may be a bit confusing. We hit the endpoint and expect RYOW. The data cache for post-45 updates, this triggers an update for route revalidate/tag/expires, which in turn refetches post 44 (auto cache option). There is no refetch for 46 because the data cache is not stale. Thus, 44 and 45 were updated while 46 kept the timestamp from build.
Note: we could've first visited the test route /revalidate/tag/expire, looked at the time, then hit the api endpoint and returned to our test route. But, that would've caused problems with the client-side router cache and the test would've failed. This is a problem you have to consider in real live world apps as well.
In short, we confirmed that calling revalidateTag with option { expire: 0 } does revalidate as read your own writes.
updateTag()(RYOW) in a server action
We're working with server actions and read your own writes now. Server actions are event driven. A user clicks a button and expects a change in the UI. Notice how this differs from the previous example. That was an outside action, this is internal. We expect an immediate update.
Here is our example:
// app/revalidate/tag/updateTag/page.tsx
export default function page() {
return (
<>
<div className='mb-8'>
<Post postId={47} fetchOptions={{}} />
<Post
postId={48}
fetchOptions={{ next: { revalidate: 1800, tags: ['post-48'] } }}
/>
<Post postId={49} fetchOptions={{ cache: 'force-cache' }} />
</div>
<UpdateTagActionButton tag='post-48' />
</>
);
}
So, 3 posts and a button: button -> server action -> updateTag.
Note: not 100% clear but 2 clicks.
Everything updates as expected. 47 (no cache) and 48 updated, 49 (force cache) did not. We also made a copy of this route without the button:
// app/revalidate/tag/updateTag-copy/page.tsx
export default function page() {
return (
<>
<Post postId={47} fetchOptions={{}} />
<Post
postId={48}
fetchOptions={{ next: { revalidate: 1800, tags: ['post-48'] } }}
/>
<Post postId={49} fetchOptions={{ cache: 'force-cache' }} />
</>
);
}
We clicked the button in the route revalidate/tag/updateTag. Then we visited our full route cache for the route copy:
.next\server\app\revalidate\tag\updateTag-copy.html
We found the prerendered HTML and saw that the timestamps were still the old ones on all posts. Next, we actually visited the route: revalidate/tag/updateTag-copy and found that all timestamps were updated. Upon revisiting the prerendered files, we saw that they had been updated as well.
So, this is interesting. The route where the server action was called from was updated immediately. Other routes that depend on the updated tag are marked as stale and get updated when a request is made for that route. No stale content is served. The server rerenders the route at request time, serves the fresh content and saves it in the full route cache. In other words, for other routes, the update is only made at request time. Hitting updateTag does not immediately update all routes statically.
Summary
revalidateTag invalidates a cache tag. Only call it from inside a route handler.
revalidateTag('tag', 'max'); // -> stale while revalidate
revalidateTag('tag', { expire: 0 }); // -> read-your-own-writes
Depending on the second parameter ("max" or "{ expire: 0 }"), it will also update all affected routes in the full route cache as stale while revalidate or read-your-own-writes.
updateTag invalidates a cache tag. Only call it from a server action. It invalidates cache tag and all affected routes in the full route cache as read-your-own-writes. However, only the route the server action was called from will immediately be statically rerendered. Other affected routes will only be statically rerendered at request time.
Some notes
- The
Nextdocs explicitly mention that it's fine to call bothrevalidatePathandupdateTagtogether to ensure data consistency. - Caching and revalidation work completely differently in development mode, so be careful.
If you want to support my writing, you can donate with paypal.

Top comments (0)