Examples (13)

Last updated Oct 12, 2026 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 it also sits in front of fetch() calls between entrypoints in the same Worker via ctx.exports. That second fact is the one that makes the rest of this page possible.

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. A hit returns the cached response without the callee running. A miss runs the callee 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 — but anything the caller hands off to the callee is cacheable independently.

Note

Only fetch() invocations go through the cache. Calling a custom RPC method on an entrypoint (for example ctx.exports.Backend.getUser(id)) bypasses the cache entirely. To cache a piece of work, expose it as a fetch handler.

That gives you a primitive you can compose. You can author a Worker as a chain of small entrypoints — auth, normalization, routing, the expensive read, the data layer — and let Workers Caching slot in wherever you want it. Each cached entrypoint is a unit of memoization with its own key, its own TTL, and its own tag namespace for purging. Anything you would want to configure about caching — 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.

The examples on this page all use the same shape: an outer (gateway) entrypoint that runs every request, plus one or more inner entrypoints that are cached. The outer entrypoint does something cheap (authenticate, rewrite a header, pick a route); the inner entrypoint does something expensive (look up data, transform it, run a Durable Object). They are written as classes in one source file, deployed as one Worker, billed as one Worker — connected by a cache stage that sits in front of the inner entrypoint.

Two rules to keep in mind

Two facts shape every pattern below. They follow directly from "the cache is in front of every entrypoint":

Disable caching on the gateway entrypoint. 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 your Wrangler configuration, and leave it on for the inner entrypoint the gateway forwards to. Using "default" for the default export:

{
    "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": {
        // The gateway runs on every request — no caching in front of it.
        "default": { "type": "worker", "cache": { "enabled": false } },
        // The inner entrypoint is the one that gets cached.
        "Inner": { "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.Inner]
type = "worker"

[exports.Inner.cache]
  enabled = true

Do not use `Cache-Control: no-store` to keep the gateway running

It might seem simpler to leave caching on for the gateway and return Cache-Control: no-store from it on every request. Do not do this. With caching enabled, each request to the gateway still consults the lower and upper cache tiers before your Worker runs — so every request pays the tiered-cache round trip only to produce an uncacheable response. That adds significant latency for no benefit. Disabling caching on the entrypoint in exports makes requests skip the cache lookup entirely and go straight to your gateway logic.

Strip request headers that would force a 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.

Both rules apply to every example below.

Cache authenticated responses

Caching authenticated APIs has historically been awkward. The standard bypass rules treat any request with an Authorization header as private and refuse to cache it — which is the safe default, but means a token-authenticated endpoint that returns identical responses to thousands of users runs your Worker every single time.

The pattern below lets you authenticate every request and still serve cache hits without running the cacheable handler:

  1. The outer (default) entrypoint receives the request and authenticates it.
  2. On success, it strips the Authorization header and forwards the request to a named entrypoint via ctx.exports.
  3. 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 CachedAPI:

{
    "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 } },
        "CachedAPI": { "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.CachedAPI]
type = "worker"

[exports.CachedAPI.cache]
  enabled = true

src/index.jsjs

import { WorkerEntrypoint } from "cloudflare:workers";

// Cached entrypoint. Workers Caching sits in front of this — on a hit,
// the cached response is returned and `fetch` below is never invoked.
export class CachedAPI extends WorkerEntrypoint {
    async fetch(request) {
        const data = await loadExpensiveData(request);

return new Response(JSON.stringify(data), {
            headers: {
                "Content-Type": "application/json",
                // All authenticated callers see this same response on a hit.
                "Cache-Control": "public, max-age=60",
            },
        });
    }
}

// Default entrypoint. Runs on every request to authenticate the caller,
// then forwards to the cached entrypoint.
export default {
    async fetch(request, env, ctx) {
        if (!(await authenticate(request, env))) {
            return new Response("Unauthorized", { status: 401 });
        }

// Strip the Authorization header before forwarding. Otherwise the
        // request would trigger Cloudflare's automatic bypass for
        // authenticated requests, and nothing would ever be cached.
        const forwarded = new Request(request);
        forwarded.headers.delete("Authorization");

// Caching is disabled for this gateway entrypoint (see the Wrangler
        // configuration above), so it runs on every request. Forward to the
        // cached CachedAPI entrypoint and return its response directly.
        return ctx.exports.CachedAPI.fetch(forwarded);
    },
};

async function authenticate(request, env) {
    const token = request.headers.get("Authorization")?.replace(/^Bearer\s+/, "");
    return token === env.API_TOKEN;
}

async function loadExpensiveData(request) {
    // Replace with your real data source — D1, KV, an origin, and so on.
    return { timestamp: Date.now() };
}

src/index.tsts

import { WorkerEntrypoint } from "cloudflare:workers";

interface Env {
    API_TOKEN: string;
}

// Cached entrypoint. Workers Caching sits in front of this — on a hit,
// the cached response is returned and `fetch` below is never invoked.
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",
                // All authenticated callers see this same response on a hit.
                "Cache-Control": "public, max-age=60",
            },
        });
    }
}

// Default entrypoint. Runs on every request to authenticate the caller,
// then forwards to the cached entrypoint.
export default {
    async fetch(request, env, ctx): Promise<Response> {
        if (!(await authenticate(request, env))) {
            return new Response("Unauthorized", { status: 401 });
        }

// Strip the Authorization header before forwarding. Otherwise the
        // request would trigger Cloudflare's automatic bypass for
        // authenticated requests, and nothing would ever be cached.
        const forwarded = new Request(request);
        forwarded.headers.delete("Authorization");

// Caching is disabled for this gateway entrypoint (see the Wrangler
        // configuration above), so it runs on every request. Forward to the
        // cached CachedAPI entrypoint and return its response directly.
        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> {
    // Replace with your real data source — D1, KV, an origin, and so on.
    return { timestamp: Date.now() };
}

A few things to notice:

  • The cache is in the right place. It sits between the outer entrypoint and the cached entrypoint, so cache hits skip the expensive work entirely. Only the auth check runs.
  • Authorization is stripped before forwarding. This is what makes the response cacheable — Cloudflare's bypass rule fires on the inbound request, not on the response, so removing the header before the request reaches the cached entrypoint is what lets the cached entrypoint's Cache-Control: public take effect. It also prevents tokens from contributing to any future cache key.
  • The cached response is shared across users. Every caller who passes the auth check sees the same cached body.

Caution

This example is intentionally minimal to show the shape of the pattern. Sharing a single cached response across every authenticated caller is only correct when the response really is the same for all of them — for example, a public catalog behind a token gate, or an internal endpoint where every caller is equally privileged. If different callers should see different data, the next section shows how to partition the cache per user with ctx.props. Workers Caching does not infer per-user isolation for you; you have to ask for it.

Per-user authenticated responses

If your endpoint returns user-specific data, pass the user identifier via ctx.props. 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 uses the same Wrangler configuration as the previous example — caching disabled on default, enabled on CachedAPI:

src/index.jsjs

import { WorkerEntrypoint } from "cloudflare:workers";

export class CachedAPI extends WorkerEntrypoint {
    async fetch(request) {
        // ctx.props.userId is part of the cache key, so this response
        // is cached separately for every userId.
        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) {
        const userId = await authenticate(request, env);
        if (!userId) {
            return new Response("Unauthorized", { status: 401 });
        }

const forwarded = new Request(request);
        forwarded.headers.delete("Authorization");

// The gateway's cache is disabled, so it runs on every request.
        // Pass the authenticated userId to the cached entrypoint via props —
        // this becomes part of the cache key.
        return ctx.exports.CachedAPI.fetch(forwarded, {
            props: { userId },
        });
    },
};

async function authenticate(request, env) {
    // Replace with your real auth — JWT verification, token lookup, and so on.
    return "user-42";
}

async function loadUserData(userId) {
    return { userId, timestamp: Date.now() };
}

src/index.tsts

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> {
        // ctx.props.userId is part of the cache key, so this response
        // is cached separately for every userId.
        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");

// The gateway's cache is disabled, so it runs on every request.
        // Pass the authenticated userId to the cached entrypoint via props —
        // this becomes part of the cache key.
        return ctx.exports.CachedAPI.fetch(forwarded, {
            props: { userId },
        });
    },
} satisfies ExportedHandler<Env>;

async function authenticate(
    request: Request,
    env: Env,
): Promise<string | null> {
    // Replace with your real auth — JWT verification, token lookup, and so on.
    return "user-42";
}

async function loadUserData(userId: string): Promise<unknown> {
    return { userId, timestamp: Date.now() };
}

For more on cache isolation between callers, refer to Multi-tenant safety with ctx.props.

The shape of this example — the outer entrypoint shapes a value (the user's identity) into the cache key by passing it through ctx.props — is the same shape the next example uses to influence a different part of the key.