revalidateTag (fetch-cache invalidation)
Server-side fetch(url, { next: { tags: ["demo"] } }) caches the upstream response and indexes it by tag. Clicking the button POSTs to a server action that calls revalidateTag("demo") — every entry tagged "demo" is dropped from FetchCache; the next page render re-fetches fresh from upstream.
Tip
<code>revalidateTag</code> returns the number of entries dropped. If you get <strong>0</strong> when the cache seemed active, check that the tag string in the <code>fetch</code> call and in <code>revalidateTag</code> match exactly — matching is case-sensitive. Invalidation does not cross worker processes: if the server restarted, the cache was already empty.
Output
Outer (live): 2026-09-06T04:05:56.180Z
Cached receivedAt: 2026-09-06T04:05:56.203Z
How to read this
- Refresh a few times: outer ticks live, cached
receivedAtstays frozen (within the 60 s TTL). - Click the button: action runs server-side,
revalidateTag("demo")drops the cached entry and reports how many entries were removed. - The action's loader rerun fetches fresh — the next
receivedAtyou see is the post-invalidation timestamp.
import { revalidateTag } from "@bext-stack/framework/cache";
export const dynamic = "force-dynamic";
export async function action({ request }: { request: Request }) {
const removed = revalidateTag("demo"); // returns count dropped
return { ok: true, removed };
}
export default async function Page({ actionData }: any) {
// Cached upstream fetch — receivedAt stays frozen until the tag
// is invalidated.
const r = await fetch("https://demo.bext.dev/api/echo?key=revalidate-tag", {
next: { revalidate: 60, tags: ["demo"] },
});
const data = await r.json();
return (
<div>
<p>Outer (live): {new Date().toISOString()}</p>
<p>Cached receivedAt: <strong>{data.receivedAt}</strong></p>
<form method="post">
<button type="submit">revalidateTag("demo")</button>
</form>
{actionData?.ok ? <p>Invalidated {actionData.removed} entries</p> : null}
</div>
);
}