INTEGRITY Dokumentace

Příklady

Workers Caching je cache, která je sama o sobě primitivem Workeru. Nachází se před každým entrypointem Workeru, tedy před výchozím exportem i každým pojmenovaným WorkerEntrypoint : a zároveň stojí před fetch() volání mezi entrypointy ve stejném Workeru přes ctx.exports. Právě tato druhá skutečnost je tím, co umožňuje zbytek této stránky.

Když jeden vstupní bod vyvolá u jiného vstupního bodu jeho fetch() prostřednictvím ctx.exports, cache vyhodnotí toto volání stejně, jako by vyhodnotila požadavek z prohlížeče. Při shodě (hit) se vrátí odpověď z cache, aniž by se volaný Worker spustil. Při neshodě (miss) se volaný Worker spustí a odpověď se uloží pod vlastním klíčem cache, odvozeným z entrypointu volaného Workeru, cesty, query stringu a ctx.props. Volající se stále spouští při každém požadavku, ale vše, co volající předá volanému, lze cachovat nezávisle.

Tím získáte primitivum, které lze skládat. Worker můžete navrhnout jako řetězec malých vstupních bodů (autentizace, normalizace, směrování, nákladné čtení, datová vrstva) a Workers Caching zapojit kamkoli potřebujete. Každý vstupní bod uložený v mezipaměti je jednotkou memoizace s vlastním klíčem, vlastní dobou TTL a vlastním jmenným prostorem značek pro purgování. Cokoli byste chtěli u ukládání do mezipaměti nastavit, tedy kdy se spustí, podle čeho se klíčuje a kdy se invaliduje, se vyjadřuje jako běžný kód Workeru: který vstupní bod zavoláte, jaký požadavek předáte, jaké ctx.props předáte, co Cache-Control nastavíte.

Všechny příklady na této stránce mají stejnou strukturu: vnější vstupní bod (gateway), který běží při každém požadavku, a jeden nebo více vnitřních vstupních bodů, které se ukládají do mezipaměti. Vnější vstupní bod provádí něco levného (ověření, přepsání hlavičky, výběr trasy), vnitřní vstupní bod provádí něco nákladného (vyhledání dat, jejich transformaci, spuštění Durable Object). Jsou napsány jako třídy v jednom zdrojovém souboru, nasazeny jako jeden Worker a účtovány jako jeden Worker, propojené fází mezipaměti umístěnou před vnitřním vstupním bodem.

Dvě pravidla, která je třeba mít na paměti

Každý z níže uvedených vzorů určují dvě skutečnosti. Přímo vyplývají z toho, že „mezipaměť stojí před každým vstupním bodem“:

Zakažte ukládání do mezipaměti u vstupního bodu brány. Protože cache je ve výchozím nastavení umístěna před každým entrypointem, byl by ukládán do mezipaměti i vnější entrypoint. Další požadavek by se pak obsloužil z této vnější cache, aniž by se vůbec dostal do logiky vaší brány. V konfiguraci Wrangleru proto vypněte ukládání do mezipaměti pro entrypoint brány a ponechte je zapnuté pro vnitřní entrypoint, na který brána požadavky předává. Pomocí "default" pro výchozí export:

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"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"
# Set this to today's date
compatibility_date = "2026-08-28"

[cache]
enabled = true

[exports.default]
type = "worker"

  [exports.default.cache]
  enabled = false

[exports.Inner]
type = "worker"

  [exports.Inner.cache]
  enabled = true

Odstraňte hlavičky požadavku, které by vynutily obejití. standardní chování Cloudflare pravidla pro obcházení se vztahují i na mezipaměť vnitřního entrypointu, konkrétně na Authorization hlavička v předávaném požadavku promění každé vnitřní volání na BYPASS, a nikdy se nic neuloží. Když vnější vstupní bod ověří požadavek a rozhodne, že je bezpečné jej uložit do cache, musí odstranit Authorization (a cokoli dalšího, co spustí automatické obejití) před voláním vnitřního entrypointu.

Obě pravidla platí pro všechny níže uvedené příklady.

Ukládání autentizovaných odpovědí do mezipaměti

Ukládání ověřených API do mezipaměti bylo historicky nepraktické. Standardní pravidla pro obcházení zacházet s jakýmkoli requestem, který má Authorization hlavičku jako soukromou a odmítne ji uložit do mezipaměti. Jde o bezpečné výchozí chování, znamená to však, že endpoint ověřený tokenem, který vrací tisícům uživatelů identické odpovědi, spustí váš Worker při každém požadavku.

Následující vzor vám umožní ověřovat každý požadavek a přitom stále obsluhovat zásahy v cache bez spuštění cacheable handleru:

  1. Vnější (výchozí) entrypoint přijímá požadavek a ověřuje jej.
  2. V případě úspěchu odstraní Authorization hlavičku a předá požadavek pojmenovanému entrypointu přes ctx.exports.
  3. Workers Caching stojí před pojmenovaným vstupním bodem. Při zásahu se odpověď z mezipaměti vrátí vnějšímu vstupnímu bodu, který ji předá klientovi, aniž by se pojmenovaný vstupní bod vůbec spustil.

Zakažte ukládání do mezipaměti u výchozího vstupního bodu, aby se spouštěl při každém požadavku kvůli ověřování, a ponechte ho zapnuté pro CachedAPI:

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"cache": { "enabled": true },
	"exports": {
		"default": { "type": "worker", "cache": { "enabled": false } },
		"CachedAPI": { "type": "worker", "cache": { "enabled": true } },
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"

[cache]
enabled = true

[exports.default]
type = "worker"

  [exports.default.cache]
  enabled = false

[exports.CachedAPI]
type = "worker"

  [exports.CachedAPI.cache]
  enabled = true
src/index.js
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.ts
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() };
}

Všimněte si několika věcí:

Ověřené odpovědi pro jednotlivé uživatele

Pokud váš endpoint vrací data specifická pro uživatele, předejte identifikátor uživatele přes ctx.props. Workers Caching zahrnuje ctx.props v cache key, takže každý uživatel získá vlastní položku v mezipaměti a jeden uživatel nikdy nemůže obdržet uloženou odpověď jiného uživatele. Používá se stejná konfigurace Wrangler jako v předchozím příkladu: ukládání do mezipaměti je vypnuté pro default, povoleno na CachedAPI:

src/index.js
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.ts
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() };
}

Více o izolaci cache mezi volajícími najdete v Bezpečnost pro více nájemců pomocí ctx.props.

Na tomto příkladu je vidět, že vnější entrypoint přetváří hodnotu (identitu uživatele) na klíč mezipaměti tím, že ji předá přes ctx.props : má stejný tvar, jaký další příklad používá k ovlivnění jiné části klíče.

Normalizovat Accept-Encoding pro Vary

Vary umožňuje, aby jedna URL ukládala do cache více reprezentací, například variantu stejného assetu kódovanou Brotli a variantu kódovanou gzip. Cloudflare rozlišuje varianty podle doslovná hodnota každého Vary-vypsaná hlavička požadavku, takže dva požadavky se sémanticky rovnocennými, ale textově odlišnými Accept-Encoding hlavičky vytvoří dvě samostatné varianty.

U požadavků směrovaných přes první linii Cloudflare na tom záleží ještě více: Accept-Encoding hlavička požadavku, kterou váš Worker vidí, bývá společností Cloudflare přepsána na kanonickou hodnotu (například gzip, br) kvůli efektivitě mezipaměti. Původní hodnota zůstává zachována v request.cf.clientAcceptEncoding, ale pokud se váš Worker liší podle Accept-Encoding aniž by se nejprve obnovila hodnota eyeballu, každá varianta uložená v mezipaměti skončí indexovaná podle přepsaného řetězce: mezipaměť tak vrací variantu Brotli klientům, kteří přijímají pouze gzip, nebo naopak.

Řešením je vstupní bod gateway, který obnoví Accept-Encoding z request.cf.clientAcceptEncoding před předáním do cachovaného entrypointu. Vypněte ukládání do cache na bráně a zapněte je na CachedAssets:

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"cache": { "enabled": true },
	"exports": {
		"default": { "type": "worker", "cache": { "enabled": false } },
		"CachedAssets": { "type": "worker", "cache": { "enabled": true } },
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"

[cache]
enabled = true

[exports.default]
type = "worker"

  [exports.default.cache]
  enabled = false

[exports.CachedAssets]
type = "worker"

  [exports.CachedAssets.cache]
  enabled = true
src/index.js
import { WorkerEntrypoint } from "cloudflare:workers";

export class CachedAssets extends WorkerEntrypoint {
	async fetch(request) {
		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",
				// One variant per distinct Accept-Encoding value the cached
				// entrypoint sees. The gateway below normalizes that value.
				Vary: "Accept-Encoding",
			},
		});
	}
}

export default {
	async fetch(request, env, ctx) {
		// On Cloudflare, the eyeball's Accept-Encoding is usually rewritten
		// to a canonical value before the Worker runs. Restore it from
		// request.cf.clientAcceptEncoding so the cached entrypoint sees
		// what the client actually sent — and so Vary keys variants on
		// the real value.
		const original = request.cf?.clientAcceptEncoding;

		const forwarded = new Request(request);
		if (original) {
			forwarded.headers.set("Accept-Encoding", original);
		}

		// The gateway's cache is disabled (see the Wrangler configuration
		// above), so it runs on every request and always restores
		// Accept-Encoding before forwarding to the cached entrypoint.
		return ctx.exports.CachedAssets.fetch(forwarded);
	},
};

async function loadBrotli(request) {
	// Replace with your real asset loader (R2, KV, fetch, and so on).
	return { body: new ArrayBuffer(0), encoding: "br" };
}

async function loadGzip(request) {
	return { body: new ArrayBuffer(0), encoding: "gzip" };
}
src/index.ts
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",
				// One variant per distinct Accept-Encoding value the cached
				// entrypoint sees. The gateway below normalizes that value.
				Vary: "Accept-Encoding",
			},
		});
	}
}

export default {
	async fetch(request, env, ctx): Promise<Response> {
		// On Cloudflare, the eyeball's Accept-Encoding is usually rewritten
		// to a canonical value before the Worker runs. Restore it from
		// request.cf.clientAcceptEncoding so the cached entrypoint sees
		// what the client actually sent — and so Vary keys variants on
		// the real value.
		const original = request.cf?.clientAcceptEncoding;

		const forwarded = new Request(request);
		if (original) {
			forwarded.headers.set("Accept-Encoding", original);
		}

		// The gateway's cache is disabled (see the Wrangler configuration
		// above), so it runs on every request and always restores
		// Accept-Encoding before forwarding to the cached entrypoint.
		return ctx.exports.CachedAssets.fetch(forwarded);
	},
} satisfies ExportedHandler;

async function loadBrotli(
	request: Request,
): Promise<{ body: ArrayBuffer; encoding: string }> {
	// Replace with your real asset loader (R2, KV, fetch, and so on).
	return { body: new ArrayBuffer(0), encoding: "br" };
}

async function loadGzip(
	request: Request,
): Promise<{ body: ArrayBuffer; encoding: string }> {
	return { body: new ArrayBuffer(0), encoding: "gzip" };
}

Čeho si všimnout:

Pokud nepotřebujete varianty podle kódování, tedy pokud Worker vždy vrací Brotli, když ho klient akceptuje, a jinak používá gzip, pak nepotřebujete Vary vůbec. Zvolte kanonické kódování uvnitř uloženého entrypointu na základě obnoveného Accept-Encoding, a nechte cache uložit jedinou variantu. Více informací najdete v Accept-Encoding a Content-Encoding pro danou variantu vzoru.

Doposud byl vnitřní vstupní bod funkcí požadavku. Následující příklad umísťuje za stejnou fázi cache stavovou komponentu, konkrétně Durable Object, se stejným tvarem.

Ukládání odpovědí Durable Object do mezipaměti

Durable Objects se nikdy neukládají přímo do mezipaměti pomocí Workers Caching: jsou stavové a ukládání jejich odpovědí do mezipaměti by bylo proti smyslu věci. Řada koncových bodů Durable Object ale obsluhuje provoz náročný na čtení, kde je krátké TTL mezipaměti naprosto v pořádku, například u žebříčků, počítadel, agregovaných statistik nebo konfigurace, která se mění jen několikrát za hodinu.

Tyto odpovědi můžete cachovat tak, že Durable Object zabalíte za pojmenovaný entrypoint a necháte Workers Caching fungovat před tímto entrypointem. Při zásahu do cache (cache hit) se wrapper vůbec nespustí a Durable Object se vůbec nepoužije. Cachování zakažte na výchozím (router) entrypointu a povolte ho na CachedLeaderboard wrapper. Samotný Durable Object se nikdy neukládá do mezipaměti a nevyžaduje žádnou konfiguraci mezipaměti:

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"cache": { "enabled": true },
	"exports": {
		"default": { "type": "worker", "cache": { "enabled": false } },
		"CachedLeaderboard": { "type": "worker", "cache": { "enabled": true } },
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"

[cache]
enabled = true

[exports.default]
type = "worker"

  [exports.default.cache]
  enabled = false

[exports.CachedLeaderboard]
type = "worker"

  [exports.CachedLeaderboard.cache]
  enabled = true
src/index.js
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";

// A Durable Object that maintains an expensive-to-compute leaderboard.
export class Leaderboard extends DurableObject {
	async fetch(request) {
		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();
			await this.record(userId, score);
			return new Response("Recorded");
		}

		return new Response("Not found", { status: 404 });
	}

	async computeTop() {
		// Pretend this is expensive — a sorted scan of stored state, an
		// aggregation across many keys, a call to another service.
		return { top: [], computedAt: Date.now() };
	}

	async record(userId, score) {
		await this.ctx.storage.put(`score:${userId}`, score);
	}
}

// Cached entrypoint. Forwards GET /top to the Durable Object and tags
// the response so it can be purged when scores change.
export class CachedLeaderboard extends WorkerEntrypoint {
	async fetch(request) {
		const id = this.env.LEADERBOARD.idFromName("global");
		const stub = this.env.LEADERBOARD.get(id);
		const response = await stub.fetch(request);

		// Copy the body and headers into a new Response so we can attach
		// cache headers. The DO's body stream is consumed once here.
		return new Response(response.body, {
			status: response.status,
			headers: {
				...Object.fromEntries(response.headers),
				"Cache-Control": "public, max-age=30",
				"Cache-Tag": "leaderboard",
			},
		});
	}

	// Invalidate this entrypoint's cached leaderboard. purge() is scoped to
	// the entrypoint that calls it, so it must run inside CachedLeaderboard —
	// the entrypoint that owns the cached response. The gateway invokes this
	// over ctx.exports after a write.
	async invalidate() {
		await this.ctx.cache.purge({ tags: ["leaderboard"] });
	}
}

// Default entrypoint. Routes reads through the cached entrypoint
// and writes directly to the Durable Object, invalidating the cache on write.
export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);

		if (request.method === "GET" && url.pathname === "/top") {
			// Read path — goes through Workers Caching. The router's cache is
			// disabled (see the Wrangler configuration above), so it runs on
			// every request. On a hit, CachedLeaderboard never runs and the
			// Durable Object is never touched.
			return ctx.exports.CachedLeaderboard.fetch(request);
		}

		if (request.method === "POST" && url.pathname === "/record") {
			// Write path — bypass the cached entrypoint, hit the Durable
			// Object directly, then ask CachedLeaderboard to invalidate its
			// own cache so the next read returns fresh data. The purge must
			// run inside CachedLeaderboard because purges are scoped to the
			// entrypoint that owns the cached response — a purge from this
			// gateway would target the gateway's (disabled) cache instead.
			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 });
	},
};
src/index.ts
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";

interface Env {
	LEADERBOARD: DurableObjectNamespace<Leaderboard>;
}

// A Durable Object that maintains an expensive-to-compute 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> {
		// Pretend this is expensive — a sorted scan of stored state, an
		// aggregation across many keys, a call to another service.
		return { top: [], computedAt: Date.now() };
	}

	private async record(userId: string, score: number): Promise<void> {
		await this.ctx.storage.put(`score:${userId}`, score);
	}
}

// Cached entrypoint. Forwards GET /top to the Durable Object and tags
// the response so it can be purged when scores change.
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);

		// Copy the body and headers into a new Response so we can attach
		// cache headers. The DO's body stream is consumed once here.
		return new Response(response.body, {
			status: response.status,
			headers: {
				...Object.fromEntries(response.headers),
				"Cache-Control": "public, max-age=30",
				"Cache-Tag": "leaderboard",
			},
		});
	}

	// Invalidate this entrypoint's cached leaderboard. purge() is scoped to
	// the entrypoint that calls it, so it must run inside CachedLeaderboard —
	// the entrypoint that owns the cached response. The gateway invokes this
	// over ctx.exports after a write.
	async invalidate(): Promise<void> {
		await this.ctx.cache.purge({ tags: ["leaderboard"] });
	}
}

// Default entrypoint. Routes reads through the cached entrypoint
// and writes directly to the Durable Object, invalidating the cache on write.
export default {
	async fetch(request, env, ctx): Promise<Response> {
		const url = new URL(request.url);

		if (request.method === "GET" && url.pathname === "/top") {
			// Read path — goes through Workers Caching. The router's cache is
			// disabled (see the Wrangler configuration above), so it runs on
			// every request. On a hit, CachedLeaderboard never runs and the
			// Durable Object is never touched.
			return ctx.exports.CachedLeaderboard.fetch(request);
		}

		if (request.method === "POST" && url.pathname === "/record") {
			// Write path — bypass the cached entrypoint, hit the Durable
			// Object directly, then ask CachedLeaderboard to invalidate its
			// own cache so the next read returns fresh data. The purge must
			// run inside CachedLeaderboard because purges are scoped to the
			// entrypoint that owns the cached response — a purge from this
			// gateway would target the gateway's (disabled) cache instead.
			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>;

Proč to funguje:

Pokud máte mnoho nezávislých instancí Durable Object, například jednu na tenanta, předejte identifikátor tenanta přes ctx.props při volání cachovaného entrypointu, stejně jako Ověřené odpovědi pro jednotlivé uživatele ano. Každý tenant má vlastní záznam v mezipaměti a purge u jednoho tenanta neinvaliduje žádný jiný.

Ukládání do mezipaměti pro origin, který nespravujete

Origin server, na kterém závisíte, občas nepatří vám. Ať už jde o API třetí strany, koncový bod SaaS, veřejný dataset nebo službu dodavatele za pomalou CDN, hlavičky mezipaměti má nastavené tak, jak se rozhodl jejich vlastník, a vy je nemůžete změnit. Třeba posílá Cache-Control: no-store pro jistotu. Možná neodešle vůbec nic. Možná agresivně ukládá do mezipaměti způsobem, který neodpovídá vzorům čtení vaší aplikace. Tak či onak platíte latenci a náklady na požadavek při každém volání.

Workers Caching vám umožňuje umístit před tento origin vlastní vrstvu mezipaměti, aniž byste na straně originu cokoli měnili. Jde o stejný vzor vnějšího a vnitřního prvku jako u zbytku této stránky: tenký vstupní bod, který předává požadavky originu, přičemž Workers Caching stojí před ním a uplatňuje Cache-Control direktivy, které zvolíte. Origin server si zachovává vlastní smlouvu o mezipaměti se zbytkem světa; váš Worker pouze přidává druhou, uživatelem řízenou vrstvu mezi vaší aplikací a tímto origin serverem. Stejně jako u ostatních vzorů zakažte ukládání do mezipaměti na bráně a povolte ho na CachedOrigin:

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"cache": { "enabled": true },
	"exports": {
		"default": { "type": "worker", "cache": { "enabled": false } },
		"CachedOrigin": { "type": "worker", "cache": { "enabled": true } },
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"

[cache]
enabled = true

[exports.default]
type = "worker"

  [exports.default.cache]
  enabled = false

[exports.CachedOrigin]
type = "worker"

  [exports.CachedOrigin.cache]
  enabled = true
src/index.js
import { WorkerEntrypoint } from "cloudflare:workers";

const ORIGIN = "https://api.example.com";

// Cached entrypoint. Fetches the upstream origin and overlays your own
// Cache-Control on the response. Workers Caching sits in front of this,
// so on a hit the upstream origin is never contacted.
export class CachedOrigin extends WorkerEntrypoint {
	async fetch(request) {
		const url = new URL(request.url);
		const upstream = new URL(url.pathname + url.search, ORIGIN);

		// Forward the request to the third-party origin. The origin's own
		// caching headers (or lack of them) are about to be overwritten —
		// they apply to the origin's relationship with the public internet,
		// not to your cache layer.
		const response = await fetch(upstream, {
			method: request.method,
			headers: request.headers,
			body: request.body,
		});

		// Replace the origin's Cache-Control with your own. This is the
		// whole point of the pattern: you decide how long Workers Caching
		// stores this response, regardless of what the origin says.
		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,
		});
	}
}

// Default entrypoint. Forwards every request through the cached entrypoint.
export default {
	async fetch(request, env, ctx) {
		// The gateway's cache is disabled (see the Wrangler configuration
		// above), so it runs on every request and forwards to the cached
		// CachedOrigin entrypoint.
		return ctx.exports.CachedOrigin.fetch(request);
	},
};
src/index.ts
import { WorkerEntrypoint } from "cloudflare:workers";

const ORIGIN = "https://api.example.com";

// Cached entrypoint. Fetches the upstream origin and overlays your own
// Cache-Control on the response. Workers Caching sits in front of this,
// so on a hit the upstream origin is never contacted.
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);

		// Forward the request to the third-party origin. The origin's own
		// caching headers (or lack of them) are about to be overwritten —
		// they apply to the origin's relationship with the public internet,
		// not to your cache layer.
		const response = await fetch(upstream, {
			method: request.method,
			headers: request.headers,
			body: request.body,
		});

		// Replace the origin's Cache-Control with your own. This is the
		// whole point of the pattern: you decide how long Workers Caching
		// stores this response, regardless of what the origin says.
		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,
		});
	}
}

// Default entrypoint. Forwards every request through the cached entrypoint.
export default {
	async fetch(request, env, ctx): Promise<Response> {
		// The gateway's cache is disabled (see the Wrangler configuration
		// above), so it runs on every request and forwards to the cached
		// CachedOrigin entrypoint.
		return ctx.exports.CachedOrigin.fetch(request);
	},
} satisfies ExportedHandler;

Co se zde děje:

Několik běžných rozšíření tohoto vzoru:

Jde o stejný stavební prvek jako u všech ostatních příkladů na této stránce. Jediný rozdíl je v tom, že „nákladná práce“, kterou cachovaný entrypoint provádí při missu, je fetch na cizí server. Kontrola nad tím, jak dlouho odpověď žije, jak je klíčována a kdy je invalidována, zůstává zcela ve vašem Workeru.

Skládání vzorů

Všechny čtyři příklady představují stejnou architekturu nahlíženou ze čtyř různých úhlů:

Vnější entrypoint Co dělá fáze cache Vnitřní entrypoint
Ověření požadavku Ukládání nákladného výpočtu do mezipaměti pro jednotlivé uživatele Načte nebo vypočítá data uživatele
Obnovit Accept-Encoding Ukládání jedné varianty pro každé skutečné kódování Načte správně zakódovaný asset
Čtení tras oproti zápisům Ukládání čtení do mezipaměti a jejich invalidace při zápisu Obaluje Durable Object pomocí Cache-Tag
Přeposlat požadavek beze změny Ukládání originu třetí strany do mezipaměti podle vlastních podmínek Načte upstream a překryje Cache-Control

Mezi jednotlivými řádky se mění pouze to, co dělá vnější entrypoint před voláním a co dělá vnitřní entrypoint při zásahu mimo cache. Fáze cache uprostřed je pokaždé stejná primitivní operace: klíčovaná podle vnitřního entrypointu, cesty požadavku a query stringu a ctx.props; nastaveno vnitřním vstupním bodem Cache-Control a Cache-Tag; zneplatněno ctx.cache.purge() z toho vstupního bodu, kterému data patří.

Právě tato jednotnost umožňuje vzory kombinovat. Nic vám nebrání je naskládat do jednoho Workeru:

Každé volání mezi těmito entrypointy prochází vlastní fází cache. Řetězec je sestaven ze stejných tří stavebních bloků: WorkerEntrypoint, ctx.exports, a Cache-Control hlavička: mezipaměť je jen jednou fází řetězce, ne samostatným systémem připojeným navíc. Cokoli byste dříve nastavovali v enginu pravidel mezipaměti, nyní zapíšete jako kód: který entrypoint se spustí, jaký požadavek se přepošle, jaké props se předají, jaká Cache-Control se vrátí a co se odstraní z mezipaměti.

Neexistuje pevně daný seznam vzorů. Workers Caching vám dává k dispozici mezipaměť mezi každým vstupním bodem Workeru, co si s tím vybudujete, je jen na vás.