← Cloudflare Workers / workers / cache
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 = trueOdstraň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:
- Vnější (výchozí) entrypoint přijímá požadavek a ověřuje jej.
- V případě úspěchu odstraní
Authorizationhlavičku a předá požadavek pojmenovanému entrypointu přesctx.exports. - 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 = trueimport { 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() };
}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í:
- Cache je na správném místě. Nachází se mezi vnějším entrypointem a cachovaným entrypointem, takže cache hit zcela přeskočí nákladnou práci. Provede se pouze ověření.
Authorizationse před předáním odstraní. Právě díky tomu je odpověď možné ukládat do mezipaměti: pravidlo Cloudflare pro obcházení mezipaměti se spouští na příchozím požadavku, ne na odpovědi, takže odstranění hlavičky předtím, než požadavek dosáhne entrypointu s mezipamětí, je to, co umožňuje entrypointu s mezipamětíCache-Control: publicse projeví. Zároveň to zabraňuje tomu, aby tokeny přispívaly k jakémukoli budoucímu cache klíči.- Odpověď uložená v cache je sdílená mezi uživateli. Každý volající, který projde ověřením, uvidí stejné tělo odpovědi uložené v cache.
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:
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() };
}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 = trueimport { 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" };
}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:
- Gateway se spouští u každého požadavku, ale je malá. Obnoví pouze jednu hlavičku a volá
ctx.exports. Nákladná práce (výběr kódování, načtení assetu) se provádí pouze při cache miss. - Varianty sdílejí společnou identitu vymazání. Vymazání pomocí tagu nebo prefixu cesty zneplatní najednou všechny varianty URL, proto musí všechny varianty používat stejný
Cache-Taghodnoty. Viz poznámky v Vyjednávání obsahu pomocíVary. - Stejný vzor platí i pro ostatní normalizovatelné hlavičky. Pokud chcete rozlišovat podle
Accept-Languagea od prohlížečů dostáváte dlouhou, složitou hodnotu, normalizujte ji na bráně (například ji zjednodušte na primární jazykový tag) ještě před předáním dál. Díky tomu zůstane rozšíření cache pod kontrolou.
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 = trueimport { 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 });
},
};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:
- Za čtení při zásahu do cache nic neplatíte. Workers Caching stojí před
CachedLeaderboard, takže zásah (hit) vrátí tělo z cache, aniž by se volal wrapper, Durable Object nebo prováděla nákladná agregace. Výchozí entrypoint se stále spustí, aby požadavek předal dál, ale jde jen o tenký router. - Zápisy okamžitě invalidují cache. Handler POST aktualizuje Durable Object a poté zavolá
ctx.exports.CachedLeaderboard.invalidate(), který spouštípurge({ tags: ["leaderboard"] })uvnitřCachedLeaderboard. Na tom záleží, protože vymazání jsou omezena na entrypoint, který je volá, the gateway's cache is disabled, so a purge issued from the gateway would not touch the entriesCachedLeaderboarduložena. Hned další GET požadavek cache nezasáhne, znovu spustí wrapper a uloží čerstvou odpověď. - Entrypoint uložený v cache vlastní smlouvu o cache. Všechny hlavičky cache-control se nastavují v
CachedLeaderboard, včetněCache-Tag, aCachedLeaderboardtaké zpřístupňujeinvalidate()metodu, která je vyčistí. Durable Object o ukládání do mezipaměti neví.
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 = trueimport { 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);
},
};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:
- Vrstva cache je vaše. originu
Cache-Controlse nahrazuje dříve, než odpověď dosáhne Workers Caching, takže TTL, direktivy čerstvosti aCache-Tagnamespace jsou zcela pod kontrolou vašeho kódu. Sami rozhodujete, kdy si mezipaměť odpověď ponechá, a sami rozhodujete, kdy ji vymažete pomocíctx.cache.purge(). - Vlastní model cachování origin serveru zůstává nedotčen. Jedině váš Worker vidí přepsanou
Cache-Control. Origin nadále obsluhuje své ostatní klienty podle caching pravidel, která zveřejnil: jeho chování ani bezpečnostní model jste nezměnili, pouze jste před něj pro svou aplikaci přidali další vrstvu. - Zásahy do mezipaměti se nikdy nedotknou originu. Workers Caching stojí před
CachedOrigin, takže zásah (hit) vrátí uloženou odpověď, aniž by se volalofetchproti upstreamu. Právě to snižuje objem požadavků na origin server a latenci každého volání z cache.
Několik běžných rozšíření tohoto vzoru:
- Hodnoty TTL pro jednotlivé prostředky. Pokud mají mít různé cesty na upstreamu různou aktuálnost, rozlište je podle
url.pathnameuvnitřCachedOrigina nastavte jinýmax-age(a jinýCache-Tag) pro každý z nich. Klíč mezipaměti už zahrnuje cestu i query string, takže každý prostředek má vlastní záznam. - Ukládání do mezipaměti pro jednotlivé uživatele. Pokud vaše aplikace ověřuje volajícího a upstream vrací data specifická pro uživatele, provádějte ověření ve vnějším entrypointu a identifikátor uživatele předávejte přes
ctx.propsnaCachedOrigin, the same shape as Ověřené odpovědi pro jednotlivé uživatele. Každý uživatel má vlastní záznam v mezipaměti a jeden uživatel nikdy nemůže obdržet odpověď z mezipaměti určenou jinému uživateli. - Stale-while-revalidate. Pokud je origin server pomalý nebo nestabilní, nastavte
Cache-Control: public, max-age=60, stale-while-revalidate=600na uloženou odpověď. Většina požadavků vrátí tělo z mezipaměti okamžitě, zatímco Workers Caching na pozadí obnovuje obsah z origin serveru. Více informací najdete v Použijtestale-while-revalidatepro obnovování s nízkou latencí. - Cílená invalidace. Označte odpovědi
Cache-Taghodnoty, které odpovídají datovému modelu vaší aplikace (napříkladCache-Tag: origin:example, product:42). Když víte, že se upstream změnil (spustí se webhook nebo proběhne administrátorská akce), zavolejtectx.cache.purge({ tags: ["product:42"] })a následující požadavek znovu naplní mezipaměť.
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:
- Vnější vstupní bod, který ověřuje a směruje.
- Normalizační entrypoint, který odstraní sledovací parametry dotazu a obnoví
Accept-Encoding, a upraví požadavek do kanonického tvaru. - Vstupní bod v cache, který stojí před Durable Object a je označen k vymazání.
- Samostatný cachovaný entrypoint pro neautentizovaný veřejný endpoint, dostupný také přes stejný vnější entrypoint, s vlastním klíčem mezipaměti a
Cache-Tagnamespace.
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.