Purge and invalidate the cache
Last updated Oct 12, 2026
Your Worker can delete its own cached responses at any time with purge(), or mark them stale with invalidate() so that the cache revalidates them with your Worker on the next request. Both are useful when data changes and the new value is more important than the performance benefit of continuing to serve the cached response — for example, after a content update, a user action, or a webhook from an upstream system.
Because Workers Caching is your Worker's cache, purging and invalidating are scoped to the Worker that owns the cache. Within a Worker, both are further scoped to the entrypoint that called purge() or invalidate(). A Worker cannot reach into another Worker's cache, an entrypoint cannot reach into another entrypoint's cache, and no zone-level purge (via the dashboard, API, or Terraform) affects Workers Caching content.
Two ways to call purge
There are two equivalent ways to trigger a purge from inside your Worker:
ctx.cache.purge(...)— available on the execution context passed to every handler. Use this when you already havectxin scope.cache.purge(...)— imported fromcloudflare:workers. Use this when you want to call purge from code that does not receivectx— for example, a utility module shared across multiple handlers, or a framework adapter that does not thread the execution context through its internals.
Both forms call into the same API and behave identically. Pick whichever reads more cleanly for your code.
src/index.jsjs
import { cache } from "cloudflare:workers";
export default {
async fetch(request, env, ctx) {
// Using the module import — no need to thread ctx through helper functions.
await cache.purge({ tags: ["blog-posts"] });
// Equivalent, using ctx directly:
// await ctx.cache.purge({ tags: ["blog-posts"] });
return new Response("Purged", { status: 200 });
},
};
src/index.tsts
import { cache } from "cloudflare:workers";
export default {
async fetch(request, env, ctx): Promise<Response> {
// Using the module import — no need to thread ctx through helper functions.
await cache.purge({ tags: ["blog-posts"] });
// Equivalent, using ctx directly:
// await ctx.cache.purge({ tags: ["blog-posts"] });
return new Response("Purged", { status: 200 });
},
} satisfies ExportedHandler;
The rest of this page uses ctx.cache.purge(...) in most examples because those examples already have ctx in scope. If you prefer the import form, substitute cache.purge(...) — nothing else changes.
Purge modes
purge() accepts either purgeEverything: true on its own, or one or both of tags and pathPrefixes:
| Field | Purges | Scope |
|---|---|---|
tags |
Every cached response tagged with one of the given values via Cache-Tag. |
Per entrypoint |
pathPrefixes |
Every cached response whose request path starts with one of the given prefixes. | Per entrypoint |
purgeEverything |
Every cached response for the entrypoint that called purge(). |
Per entrypoint |
purgeEverything is exclusive — combine tags and pathPrefixes in a single call if you want, but do not pass either alongside purgeEverything.
All three modes are scoped to the entrypoint that called purge(). A purge from PublicAPI does not affect cached responses stored by AdminAPI, even if they share tag names or path prefixes. To purge across every entrypoint of a Worker, call purge() from each entrypoint.
The returned promise resolves to a result object you can inspect to confirm success or handle failures — see Return value.
Purge after a write by calling ctx.cache.purge() at the end of any handler that mutates data:
src/index.jsjs
export default {
async fetch(request, env, ctx) {
if (request.method === "POST") {
const body = await request.json();
// Mutate your data source (D1, KV, an origin, and so on), then purge
// every cached response tagged for this post.
await ctx.cache.purge({
tags: [`post-${body.postId}`, "post-list"],
});
return new Response("Updated", { status: 200 });
}
// Handle cacheable reads here.
return new Response("Hello", {
headers: { "Cache-Control": "public, max-age=3600" },
});
},
};
src/index.tsts
export default {
async fetch(request, env, ctx): Promise<Response> {
if (request.method === "POST") {
const body = await request.json<{ postId: string }>();
// Mutate your data source (D1, KV, an origin, and so on), then purge
// every cached response tagged for this post.
await ctx.cache.purge({
tags: [`post-${body.postId}`, "post-list"],
});
return new Response("Updated", { status: 200 });
}
// Handle cacheable reads here.
return new Response("Hello", {
headers: { "Cache-Control": "public, max-age=3600" },
});
},
} satisfies ExportedHandler;
You can combine fields in a single call. For example, purge({ tags: ["blog-posts"], pathPrefixes: ["/blog/"] }) purges everything that matches either tag or path-prefix — the fields are unioned, not intersected. Use this when one logical change affects responses tagged by multiple schemes.
src/index.jsjs
export default {
async fetch(request, env, ctx) {
// Combined call: purges everything tagged "blog-posts" AND
// everything under /blog/ in a single round-trip.
await ctx.cache.purge({
tags: ["blog-posts"],
pathPrefixes: ["/blog/"],
});
return new Response("Purged", { status: 200 });
},
};
src/index.tsts
export default {
async fetch(request, env, ctx): Promise<Response> {
// Combined call: purges everything tagged "blog-posts" AND
// everything under /blog/ in a single round-trip.
await ctx.cache.purge({
tags: ["blog-posts"],
pathPrefixes: ["/blog/"],
});
return new Response("Purged", { status: 200 });
},
} satisfies ExportedHandler;
Purge by tag
Tags are attached to responses via the Cache-Tag response header, and purged later by name. This is the most flexible and commonly used purge method.
Attach tags on write
src/index.jsjs
export default {
async fetch(request) {
const url = new URL(request.url);
const postId = url.pathname.split("/").pop() ?? "unknown";
const body = { id: postId, title: `Post ${postId}` };
return new Response(JSON.stringify(body), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
"Cache-Tag": `post,post-${postId},blog`,
},
});
},
};
src/index.tsts
export default {
async fetch(request): Promise<Response> {
const url = new URL(request.url);
const postId = url.pathname.split("/").pop() ?? "unknown";
const body = { id: postId, title: `Post ${postId}` };
return new Response(JSON.stringify(body), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
"Cache-Tag": `post,post-${postId},blog`,
},
});
},
} satisfies ExportedHandler;
The Cache-Tag header value is a comma-separated list of tags. Cloudflare strips this header before returning the response to clients.
Tag values must be printable ASCII (no spaces, no Unicode), each tag is at most 1024 characters long, and a response can carry up to 1000 tags. Tag matching at purge time is case-insensitive, so Foo and foo purge the same set of responses. Invalid tags are silently dropped at storage time — the response is still cached with the remaining valid tags. Refer to Cache tag limits for the full list.
Trigger the purge
src/admin.jsjs
export default {
async fetch(request, env, ctx) {
const postId = new URL(request.url).searchParams.get("id");
if (!postId) return new Response("Missing id", { status: 400 });
await ctx.cache.purge({ tags: [`post-${postId}`] });
return new Response("Purged", { status: 200 });
},
};
src/admin.tsts
export default {
async fetch(request, env, ctx): Promise<Response> {
const postId = new URL(request.url).searchParams.get("id");
if (!postId) return new Response("Missing id", { status: 400 });
await ctx.cache.purge({ tags: [`post-${postId}`] });
return new Response("Purged", { status: 200 });
},
} satisfies ExportedHandler;
Tag scope across entrypoints
Tags are scoped to the entrypoint that called purge(). A tag named user-42 applied to responses in two different entrypoints is not purged by a single purge({ tags: ["user-42"] }) call — it only affects the entrypoint the call originated from. If you need to purge the same tag across several entrypoints, call purge() from each entrypoint, or centralize purge calls in a shared entrypoint that caches every response you later need to purge.
Use hierarchical tags
To purge groups of related responses in one call, tag each response with multiple tags representing every level of hierarchy it belongs to — sometimes called "soft tags":
src/index.jsjs
export default {
async fetch(request) {
const path = new URL(request.url).pathname;
// Build a list of hierarchical tags for the current path.
// A response at /blog/2025/02/hello gets tags for:
// _path:/blog/, _path:/blog/2025/, _path:/blog/2025/02/, _path:/blog/2025/02/hello
const segments = path.split("/").filter(Boolean);
const tags = segments.map(
(_, i) => `_path:/${segments.slice(0, i + 1).join("/")}/`,
);
const body = `<!doctype html><title>${path}</title>`;
return new Response(body, {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
"Cache-Tag": tags.join(","),
},
});
},
};
src/index.tsts
export default {
async fetch(request): Promise<Response> {
const path = new URL(request.url).pathname;
// Build a list of hierarchical tags for the current path.
// A response at /blog/2025/02/hello gets tags for:
// _path:/blog/, _path:/blog/2025/, _path:/blog/2025/02/, _path:/blog/2025/02/hello
const segments = path.split("/").filter(Boolean);
const tags = segments.map(
(_, i) => `_path:/${segments.slice(0, i + 1).join("/")}/`,
);
const body = `<!doctype html><title>${path}</title>`;
return new Response(body, {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
"Cache-Tag": tags.join(","),
},
});
},
} satisfies ExportedHandler;
Purging the tag _path:/blog/2025/ then deletes every cached response whose URL starts with /blog/2025/.
For limits on the number, length, and character set of tags, refer to Cache tag limits.
Version-specific purging
By default, Workers Caching partitions the cache by Worker version, so each deployment already starts from a cold cache and version-specific purging is unnecessary. This section applies only when you have enabled cache.cross_version_cache to share cached responses across versions. In that case, a response written by version A may still be served after version B is deployed, and you may want to purge the entries a specific version wrote — for example, after a rollback. To do that, tag each response with the version that produced it and purge that tag later.
Add the version metadata binding to your Wrangler configuration:
{
"name": "my-worker",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"cache": { "enabled": true, "cross_version_cache": true },
"version_metadata": { "binding": "CF_VERSION_METADATA" },
}
name = "my-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
[cache]
enabled = true
cross_version_cache = true
[version_metadata]
binding = "CF_VERSION_METADATA"
Then prepend the version ID to your tags:
src/index.jsjs
export default {
async fetch(request, env, ctx) {
const { id: versionId } = env.CF_VERSION_METADATA;
const postId = new URL(request.url).pathname.split("/").pop() ?? "unknown";
return new Response(JSON.stringify({ id: postId }), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
// Include the version ID as a tag so you can purge by version later.
"Cache-Tag": `post,post-${postId},v:${versionId}`,
},
});
},
};
src/index.tsts
interface Env {
CF_VERSION_METADATA: WorkerVersionMetadata;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const { id: versionId } = env.CF_VERSION_METADATA;
const postId = new URL(request.url).pathname.split("/").pop() ?? "unknown";
return new Response(JSON.stringify({ id: postId }), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
// Include the version ID as a tag so you can purge by version later.
"Cache-Tag": `post,post-${postId},v:${versionId}`,
},
});
},
} satisfies ExportedHandler<Env>;
