Cache-Control directives for cacheability
For a response to be cached, it must carry Cache-Control directives that make it cacheable. The primary directive is 'public, max-age=N', which caches the response in Cloudflare and browsers for N seconds. Responses with Cache-Control: private or no-store are not stored and produce Cf-Cache-Status: BYPASS.
Cache-Control: no-cache with stale-while-revalidate
With 'Cache-Control: no-cache, stale-while-revalidate=N', the cached body is served immediately and the Worker runs in the background. Cf-Cache-Status is UPDATING for the SWR window. This is different from inline revalidation with just no-cache.
RFC 9111 heuristic freshness for responses without Cache-Control
If a response carries no Cache-Control header at all, Workers Caching applies RFC 9111 heuristic freshness and caches default-cacheable status codes for a heuristic TTL. For example, status 200 is cached for 2 hours and status 404 for 3 minutes. To prevent this, set Cache-Control explicitly on the response.
Only GET and HEAD requests are cached
Only GET and HEAD requests are cached. All other request methods are bypassed and produce Cf-Cache-Status: BYPASS. GET and HEAD requests for the same URL share the same cache entry.
Automatic cache bypass conditions
Cloudflare bypasses the cache when: (1) the response includes a Set-Cookie header, or (2) the request includes an Authorization header, unless the response explicitly sets Cache-Control: public, must-revalidate, or s-maxage.
Set-Cookie prevents caching
If a Worker unconditionally sets Set-Cookie (for example, a session cookie on every response), the response is never cached. To cache responses, either remove the cookie from cacheable responses, or separate cookie-setting and cacheable responses into different routes.
Status codes not cacheable by default
Workers Caching follows RFC 9111. Responses with status codes not cacheable by default (for example, 401, 403, 500) are not stored unless explicitly marked with cacheable directives.
cross_version_cache enables cache sharing across deployments
Enable cache.cross_version_cache to share cached responses across Worker versions and avoid resetting the cache on every deploy. This is useful if you deploy frequently and responses rarely change between deployments. The trade-off is that cache-affecting changes no longer apply immediately.
Cache-Control: no-cache behavior
A response with Cache-Control: no-cache is stored but Cloudflare treats every subsequent request as stale and consults your Worker before serving. With just 'Cache-Control: no-cache', every subsequent request triggers inline revalidation, producing Cf-Cache-Status: REVALIDATED if the Worker returns 304 Not Modified (body served from cache), or EXPIRED if the Worker returns a fresh 200 (body replaced).
Accept-Encoding normalization for Vary
Vary lets a single URL cache multiple representations (for example, Brotli-encoded and gzip-encoded variants). Cloudflare keys variants on the verbatim value of each Vary-listed request header. For requests routed through Cloudflare's front line, the Accept-Encoding request header seen by the Worker has typically been rewritten by Cloudflare to a canonical value (such as 'gzip, br') for cache efficiency. The original value is preserved at request.cf.clientAcceptEncoding. If the Worker varies on Accept-Encoding without restoring the eyeball's value first, every cached variant ends up keyed on the rewritten string, potentially serving incorrect encodings. Restore Accept-Encoding from request.cf.clientAcceptEncoding in a gateway entrypoint before forwarding to the cached entrypoint.
Caching uncontrolled origins pattern
To cache responses from a third-party origin whose caching headers you cannot control, use a thin entrypoint that forwards to the origin with Workers Caching sitting in front of it. The pattern is: the outer (gateway) entrypoint forwards to the cached entrypoint, which fetches the upstream origin and overlays your own Cache-Control on the response. Replace the origin's Cache-Control with your own before the response reaches Workers Caching, so the TTL, freshness directives, and Cache-Tag namespace are all controlled by your code. The origin's own caching model remains untouched; only your Worker sees the rewritten Cache-Control. Disable caching on the gateway and enable it on the cached origin entrypoint.
Composing cache patterns
Multiple caching patterns can be composed in a single Worker: an outer entrypoint that authenticates and routes, a normalization entrypoint that strips tracking query parameters and restores Accept-Encoding, a cached entrypoint that fronts a Durable Object, and separate cached entrypoints for different endpoints. Each call between entrypoints goes through its own cache stage. The cache is a stage of the chain rather than a separate system bolted on. The control over when it runs, what it keys on, when it invalidates—is expressed as ordinary Worker code: which entrypoint you call, what request you forward, what ctx.props you pass, what Cache-Control you set.
Example: Cache authenticated responses
import { WorkerEntrypoint } from "cloudflare:workers";
interface Env {
API_TOKEN: string;
}
export class CachedAPI extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
const data = await loadExpensiveData(request);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60",
},
});
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
if (!(await authenticate(request, env))) {
return new Response("Unauthorized", { status: 401 });
}
const forwarded = new Request(request);
forwarded.headers.delete("Authorization");
return ctx.exports.CachedAPI.fetch(forwarded);
},
} satisfies ExportedHandler<Env>;
async function authenticate(request: Request, env: Env): Promise<boolean> {
const token = request.headers.get("Authorization")?.replace(/^Bearer\s+/, "");
return token === env.API_TOKEN;
}
async function loadExpensiveData(request: Request): Promise<unknown> {
return { timestamp: Date.now() };
}
Example: Per-user cached responses with ctx.props
import { WorkerEntrypoint } from "cloudflare:workers";
interface Env {
API_TOKEN: string;
}
interface Props {
userId: string;
}
export class CachedAPI extends WorkerEntrypoint<Env, Props> {
async fetch(request: Request): Promise<Response> {
const { userId } = this.ctx.props;
const data = await loadUserData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60",
},
});
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const userId = await authenticate(request, env);
if (!userId) {
return new Response("Unauthorized", { status: 401 });
}
const forwarded = new Request(request);
forwarded.headers.delete("Authorization");
return ctx.exports.CachedAPI.fetch(forwarded, {
props: { userId },
});
},
} satisfies ExportedHandler<Env>;
async function authenticate(request: Request, env: Env): Promise<string | null> {
return "user-42";
}
async function loadUserData(userId: string): Promise<unknown> {
return { userId, timestamp: Date.now() };
}
Example: Normalize Accept-Encoding for Vary
import { WorkerEntrypoint } from "cloudflare:workers";
export class CachedAssets extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const accept = request.headers.get("Accept-Encoding") ?? "";
const wantsBrotli = accept.includes("br");
const { body, encoding } = wantsBrotli
? await loadBrotli(request)
: await loadGzip(request);
return new Response(body, {
headers: {
"Content-Type": "application/javascript",
"Content-Encoding": encoding,
"Cache-Control": "public, max-age=86400, immutable",
Vary: "Accept-Encoding",
},
});
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const original = request.cf?.clientAcceptEncoding;
const forwarded = new Request(request);
if (original) {
forwarded.headers.set("Accept-Encoding", original);
}
return ctx.exports.CachedAssets.fetch(forwarded);
},
} satisfies ExportedHandler;
async function loadBrotli(request: Request): Promise<{ body: ArrayBuffer; encoding: string }> {
return { body: new ArrayBuffer(0), encoding: "br" };
}
async function loadGzip(request: Request): Promise<{ body: ArrayBuffer; encoding: string }> {
return { body: new ArrayBuffer(0), encoding: "gzip" };
}
Example: Cache Durable Object responses
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
interface Env {
LEADERBOARD: DurableObjectNamespace<Leaderboard>;
}
export class Leaderboard extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/top") {
const top = await this.computeTop();
return new Response(JSON.stringify(top), {
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname === "/record" && request.method === "POST") {
const { userId, score } = await request.json<{ userId: string; score: number }>();
await this.record(userId, score);
return new Response("Recorded");
}
return new Response("Not found", { status: 404 });
}
private async computeTop(): Promise<unknown> {
return { top: [], computedAt: Date.now() };
}
private async record(userId: string, score: number): Promise<void> {
await this.ctx.storage.put(`score:${userId}`, score);
}
}
export class CachedLeaderboard extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
const id = this.env.LEADERBOARD.idFromName("global");
const stub = this.env.LEADERBOARD.get(id);
const response = await stub.fetch(request);
return new Response(response.body, {
status: response.status,
headers: {
...Object.fromEntries(response.headers),
"Cache-Control": "public, max-age=30",
"Cache-Tag": "leaderboard",
},
});
}
async invalidate(): Promise<void> {
await this.ctx.cache.purge({ tags: ["leaderboard"] });
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
if (request.method === "GET" && url.pathname === "/top") {
return ctx.exports.CachedLeaderboard.fetch(request);
}
if (request.method === "POST" && url.pathname === "/record") {
const id = env.LEADERBOARD.idFromName("global");
const stub = env.LEADERBOARD.get(id);
const result = await stub.fetch(request);
await ctx.exports.CachedLeaderboard.invalidate();
return result;
}
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
Example: Cache an uncontrolled origin
import { WorkerEntrypoint } from "cloudflare:workers";
const ORIGIN = "https://api.example.com";
export class CachedOrigin extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const upstream = new URL(url.pathname + url.search, ORIGIN);
const response = await fetch(upstream, {
method: request.method,
headers: request.headers,
body: request.body,
});
const headers = new Headers(response.headers);
headers.set("Cache-Control", "public, max-age=300");
headers.set("Cache-Tag", "origin:example");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
return ctx.exports.CachedOrigin.fetch(request);
},
} satisfies ExportedHandler;
Multi-tenant cache isolation with ctx.props
When you have many independent instances (for example, one Durable Object per tenant), pass the tenant identifier via ctx.props when invoking the cached entrypoint. Each tenant gets its own cache entry, and a purge on one tenant does not invalidate any other.
Caching Durable Object responses pattern
Durable Objects are never cached directly by Workers Caching because they are stateful and caching would defeat the point. However, read-heavy Durable Object endpoints where a short cache TTL is acceptable (leaderboards, counters, aggregated stats, configuration) can be cached by wrapping the Durable Object behind a named entrypoint with Workers Caching in front of it. On a cache hit, the wrapper never runs and the Durable Object is never touched. Disable caching on the default (router) entrypoint and enable it on the cached wrapper entrypoint. The Durable Object itself is never cached and needs no cache configuration.
Workers Caching sits in front of every entrypoint
Workers Caching is a cache that is itself a Worker primitive. It sits in front of every Worker entrypoint (the default export and every named WorkerEntrypoint) and also sits in front of fetch() calls between entrypoints in the same Worker via ctx.exports.
Cache hits never touch the origin
When caching an uncontrolled origin using Workers Caching in front of a cached entrypoint, cache hits return the stored response without invoking fetch against the upstream. This cuts origin request volume and latency of every cached call.
Cache evaluation for fetch() between entrypoints
When one entrypoint invokes another's fetch() via ctx.exports, the cache evaluates that call the same way it would evaluate a request from a browser. On a cache hit, the cached response is returned without the callee running. On a miss, the callee runs and stores the response under its own cache key, keyed by the callee's entrypoint, path, query string, and ctx.props. The caller still runs on every request.
Disable caching on gateway entrypoints
Because the cache sits in front of every entrypoint by default, the outer entrypoint would itself be cached and the next request would be served from that outer cache without ever entering your gateway logic. Turn caching off for the gateway entrypoint in Wrangler configuration with 'cache': { 'enabled': false }, and leave it on for the inner entrypoint the gateway forwards to.
Do not use Cache-Control: no-store to keep gateway running
Do not leave caching on for the gateway and return Cache-Control: no-store from it on every request. With caching enabled, each request to the gateway still consults the lower and upper cache tiers before the Worker runs, so every request pays the tiered-cache round trip only to produce an uncacheable response, adding significant latency for no benefit. Disabling caching on the entrypoint in exports makes requests skip the cache lookup entirely and go straight to gateway logic.
Strip request headers that would force bypass
Cloudflare's standard bypass rules apply to the inner entrypoint's cache too. An Authorization header on the forwarded request will turn every inner call into a BYPASS, and nothing will ever be stored. When the outer entrypoint authenticates the request and decides it is safe to cache, it must strip Authorization (and anything else that triggers automatic bypass) before invoking the inner entrypoint.
Cache authenticated responses pattern
To cache authenticated APIs: the outer (default) entrypoint receives the request and authenticates it. On success, it strips the Authorization header and forwards the request to a named entrypoint via ctx.exports. Workers Caching sits in front of the named entrypoint. On a hit, the cached response is returned to the outer entrypoint, which returns it to the client, without the named entrypoint ever running. Disable caching on the default entrypoint so it runs on every request to authenticate, and keep it on for the cached entrypoint.
Per-user authenticated responses using ctx.props
If an endpoint returns user-specific data, pass the user identifier via ctx.props when invoking the cached entrypoint. Workers Caching includes ctx.props in the cache key, so each user gets their own cache entry and one user can never receive another user's cached response. This allows sharing a cached endpoint across authenticated users while maintaining per-user isolation.
Wrangler configuration for gateway pattern
To use the gateway pattern with Workers Caching, configure exports in wrangler.json with cache settings. The default (gateway) entrypoint should have 'cache': { 'enabled': false } and inner entrypoints should have 'cache': { 'enabled': true }. Example: { 'name': 'my-worker', 'main': 'src/index.ts', 'compatibility_date': '$today', 'cache': { 'enabled': true }, 'exports': { 'default': { 'type': 'worker', 'cache': { 'enabled': false } }, 'CachedAPI': { 'type': 'worker', 'cache': { 'enabled': true } } } }
cross_version_cache configuration
Set cache.cross_version_cache to true to drop the version from the cache key and share cached responses across versions. A response written by version A is then still served after version B is deployed, as long as its TTL has not expired. This maximizes cache hit rate but slows rollouts: a change that alters response content will not take effect for already-cached entries until they expire or you purge them.
Invalidating cache after deploy with cross_version_cache
When cross_version_cache is enabled and you need a deployment to take effect immediately, you have two options: tag each cached response with the Worker version (using the version metadata binding to read the current version ID at request time and prepend it as a Cache-Tag value, then purge that version tag on rollback), or purge everything after each deploy by calling ctx.cache.purge({ purgeEverything: true }) from a small Worker endpoint triggered by your CI.
Example: wrangler.json for per-entrypoint caching with custom cache key pattern
```jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "$today",
"cache": { "enabled": true },
"exports": {
"default": { "type": "worker", "cache": { "enabled": false } },
"Backend": { "type": "worker", "cache": { "enabled": true } },
},
}
```
This configuration shows how to use per-entrypoint caching when implementing the custom cache key pattern: the default (gateway) entrypoint has caching disabled since it should run on every request, while the Backend entrypoint has caching enabled.
Zone-level cache configuration alternatives for Workers Caching
For Workers Caching, set Cache-Control headers in the Worker or branch on the request to return different headers per path (instead of Cache Rules). Shape the cache key by shaping the request, such as by rewriting the URL or setting ctx.props (instead of cache key customization in Cache Rules). Use Cache-Control headers on the response to express intent at a per-request level (instead of zone-level cache level settings). Workers Caching caches any response whose headers say it is cacheable, regardless of file extension (instead of zone's default cached-file-extensions list). Transform the request or response in the Worker's code before returning it (instead of rulesets that modify before cache).
cf properties behavior on fetch() vs Workers Caching
The cf properties on a Request behave differently: cf.cacheKey is supported on both outgoing fetch() to origin and on ctx.exports.<Entrypoint>.fetch(). cf.cacheControl is supported on both. cf.cacheTtl is supported on outgoing fetch() to origin but not on Workers Caching—set the TTL by returning Cache-Control: max-age=N (or s-maxage=N) from the callee, or by overriding it with cf.cacheControl from the caller. cf.cacheEverything is supported on outgoing fetch() to origin but not on Workers Caching—Workers Caching decides cacheability from the response's Cache-Control with no override to force-cache an otherwise uncacheable response.
Cache API is separate from Workers Caching
The Cache API (caches.default) is a separate programmatic cache store independent of Workers Caching. Operations on one do not affect the other, and ctx.cache.purge() invalidates Workers-Caching entries only. The Cache API does not read through—responses are only cached when explicitly calling put(), and every request still executes the Worker. It does not collapse concurrent requests for the same resource or participate in tiered caching. For new Workers, prefer Workers Caching. The Cache API remains useful when fine-grained programmatic control is needed.
Workers Caching vs fetch() subrequest caching
Workers Caching is a server-side cache in front of the Worker. It is separate from the cache that sits in front of outgoing fetch() subrequests the Worker makes to its own origins. A fetch() subrequest hit saves a trip to the origin, while a Workers Caching hit saves the Worker from running at all. The two operate independently.
Workers Caching is separate from zone-level cache
Workers Caching is a Worker's own cache, not the zone's cache. It uses the Worker itself as the configuration surface. Zone-level features like Cache Rules, Cache Response Rules, cache key customization, zone cache level settings, default cached-file-extensions, custom tiered cache topologies, and rulesets do not apply to Workers Caching.
Cache tags in Python Workers
In Python Workers, cache tags are passed to fetch using the cf parameter as a dictionary: fetch(url, cf={'cacheTags': tags}), where tags is a list of strings.
Send cache tags via fetch in Workers
To send cache tags when making a fetch request in a Worker, pass a cf object with a cacheTags array in the init parameter of the fetch call. The cacheTags property should be an array of strings representing the tags to associate with the cached response.
Cache tags fetch init example
JavaScript example showing how to pass cache tags to fetch:
```js
const init = {
cf: {
cacheTags: tags,
},
};
return fetch(url, init);
```
Where tags is an array of strings. This attaches cache tags to the response when fetching content from an origin.
Two ways to interact with Cloudflare Cache using Workers
There are two ways to interact with Cloudflare's Cache using a Worker: (1) Call to fetch() in a Workers script. Requests proxied through Cloudflare are cached even without Workers according to a zone's default or configured behavior. Workers can customize this behavior by setting Cloudflare cache rules (operating on the cf object of a request). (2) Store responses using the Cache API from a Workers script. This allows caching responses that did not come from an origin and provides finer control by customizing cache behavior of any asset by setting headers such as Cache-Control on the response passed to cache.put() and caching responses generated by the Worker itself through cache.put().
Browser cache controlled by Cache-Control header
The browser cache is controlled through the Cache-Control header sent in the response to the client (the Response instance returned from the handler). Workers can customize browser cache behavior by setting this header on the response.
When to use fetch versus Cache API for caching
For requests where Workers are behaving as middleware (Workers are sending a subrequest via fetch), it is recommended to use fetch because preexisting settings are in place that optimize caching while preventing unintended dynamic caching. For projects where there is no backend (the entire project is on Workers as in Workers Sites), the Cache API is the only option to customize caching. The asset will be cached under the hostname specified within the Worker's subrequest, not the Worker's own hostname.
Use cases for Cache API
Use the Cache API when you want to programmatically save and/or delete responses from a cache. For example, when an origin is responding with a Cache-Control: max-age:0 header and cannot be changed, you can clone the Response, adjust the header to max-age=3600, and then use the Cache API to save the modified Response for an hour. Also use the Cache API when you want to programmatically access a Response from a cache without relying on a fetch request. For example, you can check if you have already cached a Response for a specific endpoint and avoid a slow request if it exists.
Static assets caching behavior with tiered caching
Cloudflare provides automatic caching for static assets across its network. On first request, an asset is fetched from storage and cached at the nearest Cloudflare location. For subsequent requests, Cloudflare's tiered caching system allows retrieval from a nearby cache rather than storage, improving cache hit ratio and reducing latency.
Named caches in Miniflare
Named caches can be accessed using caches.open("cache_name"). You cannot name a cache "default" as trying to do so will throw an error.
Cache persistence in Miniflare default behavior
By default, cached data in Miniflare is stored in memory. It persists between reloads of the same Miniflare instance, but not between different Miniflare instances.
Default cache access in Miniflare
Access to the default cache is enabled by default. You can access it using the caches.default.match() method within a fetch event listener.
Enable cache persistence to filesystem in Miniflare
To enable cache persistence to the file system, use the cachePersist option when creating a Miniflare instance. Set cachePersist: true to use the default path (./.mf/cache), or cachePersist: "./path" to specify a custom path.
Disable cache in Miniflare
Set the cache option to false when creating a Miniflare instance to disable both default and named caches. When disabled, caches remain available in the sandbox but do not cache anything.