Workers Cache·续

src/index.jsjs

export default {
    async fetch(request) {
        const accept = request.headers.get("Accept") ?? "";
        const wantsWebp = accept.includes("image/webp");

const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();

return new Response(body, {
            headers: {
                "Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
                "Cache-Control": "public, max-age=3600",
                // Cache a separate variant per distinct Accept header value.
                Vary: "Accept",
            },
        });
    },
};

src/index.tsts

export default {
    async fetch(request): Promise<Response> {
        const accept = request.headers.get("Accept") ?? "";
        const wantsWebp = accept.includes("image/webp");

const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();

return new Response(body, {
            headers: {
                "Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
                "Cache-Control": "public, max-age=3600",
                // Cache a separate variant per distinct Accept header value.
                Vary: "Accept",
            },
        });
    },
} satisfies ExportedHandler;

Notes:

  • Vary: * disables caching for the response. A wildcard variance cannot be satisfied deterministically from request headers, so Cloudflare does not store the response.
  • Variants share a single cache entry for purge purposes — purging a tag or path prefix that matches any variant invalidates all variants of that URL. All variants of a URL must therefore use the same Cache-Tag values.
  • Vary is not compatible with image-transformation features that already produce their own variants (Polish, Image Resizing). Responses rewritten by those features ignore Vary.
  • Variants are stored per exact request-header value. Clients that send semantically equivalent but textually different values — for example Accept-Encoding: gzip, br and Accept-Encoding: br, gzip — produce separate variants. Shape the headers your Worker sees (for example, by normalizing them in a gateway Worker before passing the request on) if you need to reduce variant fan-out.

Caching between Workers

When one Worker calls another over a service binding, the callee's cache is consulted. If the callee has caching enabled and has a matching cached response, the caller receives it without invoking the callee.

flowchart LR
    accTitle: Cache between Workers
    accDescr: Worker A calls Worker B; Worker B's cache is consulted before Worker B runs.

Request["Request"] --> WorkerA["Worker A"]
    WorkerA --> CacheB{"Worker B's cache"}
    CacheB -- Hit --> WorkerA
    CacheB -- Miss --> WorkerB["Worker B"]
    WorkerB --> CacheB

The cache key for service binding calls includes the caller's ctx.props, so different callers with different authorization context are cached separately. For details, refer to Cache keys.

For same-account calls, the calling Worker can also tailor the callee's caching for an individual request by setting cf.cacheKey to override the cache key or cf.cacheControl to supply a Cache-Control directive.

Cache Durable Object responses

Durable Objects are never cached directly by Workers Caching. However, since Workers Caching runs in front of any Worker entrypoint, you can cache a Durable Object's HTTP responses by wrapping the Durable Object behind a named Worker entrypoint and caching the entrypoint.

The wrapper entrypoint forwards the request into the Durable Object and sets Cache-Control on the response it returns. Because Workers Caching sits in front of the entrypoint, subsequent requests are served from cache without re-entering the Durable Object.

The default entrypoint here is a gateway that should run on every request, so disable caching on it and enable it on CachedCounter (see Per-entrypoint caching):

{
    "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 },
    "exports": {
        "default": { "type": "worker", "cache": { "enabled": false } },
        "CachedCounter": { "type": "worker", "cache": { "enabled": true } },
    },
}
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

[exports.default]
type = "worker"

[exports.default.cache]
  enabled = false

[exports.CachedCounter]
type = "worker"

[exports.CachedCounter.cache]
  enabled = true

src/index.jsjs

import { WorkerEntrypoint } from "cloudflare:workers";

// Cached entrypoint. Requests to this entrypoint are served from cache
// when possible; on a miss, the Durable Object is invoked and its
// response is stored.
export class CachedCounter extends WorkerEntrypoint {
    async fetch(request) {
        const id = this.env.COUNTER.idFromName("global");
        const stub = this.env.COUNTER.get(id);
        const response = await stub.fetch(request);

// Attach cache headers. Clone into a new Response so the headers
        // are mutable.
        return new Response(response.body, {
            status: response.status,
            headers: {
                ...Object.fromEntries(response.headers),
                "Cache-Control": "public, max-age=30",
            },
        });
    }
}

// Default entrypoint. Delegates to the cached entrypoint via ctx.exports,
// which routes through the cache.
export default {
    async fetch(request, env, ctx) {
        return ctx.exports.CachedCounter.fetch(request);
    },
};

src/index.tsts

import { WorkerEntrypoint } from "cloudflare:workers";

interface Env {
    COUNTER: DurableObjectNamespace;
}

// Cached entrypoint. Requests to this entrypoint are served from cache
// when possible; on a miss, the Durable Object is invoked and its
// response is stored.
export class CachedCounter extends WorkerEntrypoint<Env> {
    async fetch(request: Request): Promise<Response> {
        const id = this.env.COUNTER.idFromName("global");
        const stub = this.env.COUNTER.get(id);
        const response = await stub.fetch(request);

// Attach cache headers. Clone into a new Response so the headers
        // are mutable.
        return new Response(response.body, {
            status: response.status,
            headers: {
                ...Object.fromEntries(response.headers),
                "Cache-Control": "public, max-age=30",
            },
        });
    }
}

// Default entrypoint. Delegates to the cached entrypoint via ctx.exports,
// which routes through the cache.
export default {
    async fetch(request, env, ctx): Promise<Response> {
        return ctx.exports.CachedCounter.fetch(request);
    },
} satisfies ExportedHandler<Env>;

For more patterns that combine a gateway entrypoint with cached inner entrypoints, refer to Examples.

Smart placement and the cache

Smart Placement moves where your Worker runs when it runs — typically closer to a slow origin or database. It does not move the cache. Workers Caching always has a lower tier near the eyeball and an upper tier aggregating the network, exactly as described in Tiered cache above, whether or not Smart Placement is enabled.

The cache is always consulted before Smart Placement is considered. Concretely:

  • Lower-tier hit: the response is returned from the data center nearest the eyeball. Your Worker does not run. Smart Placement is not consulted.
  • Lower-tier miss, upper-tier hit: the response is returned from the upper tier. Your Worker does not run. Smart Placement is not consulted.
  • Both tiers miss: Smart Placement routes execution of your Worker to the placement target (for example, near your origin). The resulting response is stored in both cache tiers on the way back to the eyeball.

Importantly, the upper tier and the Smart Placement target are independent locations. The upper tier is chosen by Cloudflare to aggregate cache fills across the network; the Smart Placement target is chosen to minimize latency between your Worker and its backend. They are generally not in the same data center.

flowchart LR
    accTitle: Tiered cache with Smart Placement across three locations
    accDescr: The eyeball, the upper-tier cache, and the Smart Placement target are three independent locations. Requests traverse them in order on a full cache miss.

subgraph EyeballColo["Data center near eyeball"]
        Request["Request"] --> Lower{"Lower-tier cache"}
    end

subgraph UpperColo["Upper-tier data center"]
        Upper{"Upper-tier cache"}
    end

subgraph PlacedColo["Smart Placement target"]
        Placed["Worker runs"]
        Origin["Origin / backend"]
        Placed <--> Origin
    end

Lower -- Hit --> Response["Response"]
    Lower -- Miss --> Upper
    Upper -- Hit --> Lower
    Upper -- Miss --> Placed
    Placed --> Upper

On a full cache miss, a request therefore traverses three locations: the lower-tier data center near the eyeball, the upper-tier data center, and the Smart Placement target. The cache tiers absorb this cost so that the slow trip to the placement target is only paid once for the whole network — the upper tier shields the placement target from every lower-tier miss.

Future optimization

Because the upper tier and the Smart Placement target are chosen independently today, a cache miss pays for an extra hop between them. Future updates may bring these choices closer together — for example, by co-locating the upper tier with the placement target, or by having a single placement decision account for both — so that full cache misses incur one wide-area trip instead of two.

Purging the cache

Your Worker can purge its own cache at any time using ctx.cache.purge(). Tags are the most flexible mechanism — tag responses with Cache-Tag when returning them, and purge those tags later:

src/index.jsjs

export default {
    async fetch(request, env, ctx) {
        await ctx.cache.purge({ tags: ["blog-posts"] });
        return new Response("Purged", { status: 200 });
    },
};

src/index.tsts

export default {
    async fetch(request, env, ctx): Promise<Response> {
        await ctx.cache.purge({ tags: ["blog-posts"] });
        return new Response("Purged", { status: 200 });
    },
} satisfies ExportedHandler;

You can also import cache from cloudflare:workers and call cache.purge({...}) when you do not have ctx in scope — for example, from a utility module. To mark cached responses stale instead of deleting them, call ctx.cache.invalidate() with the same options. The cache then revalidates them with your Worker, which can answer 304 Not Modified to keep the cached body. For all purge modes and patterns, refer to Purge and invalidate the cache.

Pricing

Workers Cache has no separate pricing. When you enable Workers Cache, all requests to your Worker are billed at the standard Workers request rate — the same per-request rate as any other request to your Worker — whether the response comes from cache or from your Worker. There is no charge beyond the standard request rate. CPU time is only billed when your Worker runs — cache hits do not consume CPU time.

Caching bills requests that are normally free

When caching is enabled, every request to your Worker is charged at the standard Workers request rate, including requests that are normally free: static asset requests and worker-to-worker invocations through service bindings or ctx.exports.

Request type Request charge CPU time charge
Cache HIT (Worker does not run) Standard rate Not billed
Cache MISS (Worker runs) Standard rate Billed
Cache BYPASS (Worker runs) Standard rate Billed
Static asset request Standard rate Not billed
Worker-to-worker invocation Standard rate Billed if Worker runs

For an example, refer to Pricing example: Worker with caching.

Next steps

Was this helpful?

YesNo

On this page

Workers Cache·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/#page","headline":"Workers Cache","description":"Workers Cache lets you cache Worker responses to reduce latency and Workers usage.","url":"https://developers.cloudflare.com/workers/cache/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/cache/og.png?v=3615f4ac12f103bd","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: How Workers Caching builds cache keys, with guidance for service bindings, multi-tenant Workers, and gradual deployments. title: Cache keys image: https://developers.cloudflare.com/workers/cache/cache-keys/og.png?v=be4ec8a6bbecd55f


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.