← Cloudflare Workers / workers / cache
Примеры
Кеширование Workers кэш, который сам является примитивом Worker. Он находится перед каждой точкой входа Worker: перед экспортом по умолчанию и перед каждым именованным WorkerEntrypoint : а также стоит перед fetch() вызовов между точками входа в рамках одного Worker через ctx.exports. Именно второй факт делает возможным всё, что описано далее на этой странице.
Когда одна точка входа вызывает у другой точки входа fetch() через ctx.exports, кеш обрабатывает такой вызов так же, как обрабатывал бы запрос от браузера. При попадании в кеш возвращается кешированный ответ без выполнения вызываемой функции. При промахе вызываемая функция выполняется, а ответ сохраняется под собственным ключом кеша, который формируется из entrypoint вызываемой функции, пути, строки запроса и ctx.props. Вызывающая функция по-прежнему выполняется при каждом запросе, но всё, что она передаёт вызываемой функции, можно кэшировать независимо.
Это даёт вам примитив, из которого можно собирать более сложные решения. Worker можно оформить как цепочку небольших точек входа (аутентификация, нормализация, маршрутизация, дорогостоящее чтение, слой данных), а кеширование Workers Caching включить в любом нужном месте. Каждая кэшируемая точка входа представляет собой единицу мемоизации со своим ключом, своим TTL и своим пространством тегов для очистки. Всё, что может понадобиться настроить в кешировании (когда оно срабатывает, по чему строится ключ, когда происходит сброс), выражается обычным кодом Worker: какую точку входа вы вызываете, какой запрос передаёте, какое ctx.props вы передаёте, что Cache-Control вы задали.
Все примеры на этой странице построены по одной схеме: внешняя точка входа (шлюз), которая выполняется при каждом запросе, и одна или несколько внутренних точек входа, которые кешируются. Внешняя точка входа выполняет что-то простое (аутентификация, изменение заголовка, выбор маршрута), а внутренняя точка входа выполняет что-то затратное (получение данных, их преобразование, обращение к Durable Object). Они оформлены как классы в одном исходном файле, разворачиваются как один Worker и тарифицируются как один Worker: это достигается благодаря этапу кеширования перед внутренней точкой входа.
Два правила, о которых нужно помнить
Два факта определяют каждый паттерн ниже. Они напрямую следуют из утверждения «кеш находится перед каждой точкой входа»:
Отключите кеширование для entrypoint шлюза. Поскольку по умолчанию кеш находится перед каждой точкой входа, сам внешний entrypoint тоже будет кешироваться, и следующий запрос будет обслуживаться из этого внешнего кеша, минуя логику вашего шлюза. Отключите кеширование для entrypoint шлюза в конфигурации Wrangler и оставьте его включенным для внутреннего entrypoint, на который шлюз перенаправляет запросы. Используя "default" для экспорта по умолчанию:
{
"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Удаление заголовков запроса, которые могли бы привести к обходу. стандартный для Cloudflare правила обхода также применяются к кешу внутреннего entrypoint: Authorization заголовок в перенаправленном запросе превратит каждый внутренний вызов в BYPASS, и ничего не будет сохраняться. Когда внешняя точка входа (entrypoint) аутентифицирует запрос и решает, что его можно безопасно кэшировать, она должна удалить Authorization (и всё остальное, что вызывает автоматический обход кеша) перед вызовом внутренней точки входа.
Оба правила применяются ко всем примерам ниже.
Кеширование аутентифицированных ответов
Кеширование аутентифицированных API исторически было неудобным. Стандартный правила обхода обрабатывать любой запрос с Authorization заголовок как приватный и отказывается его кэшировать. Это безопасное поведение по умолчанию, но оно означает, что эндпоинт с аутентификацией по токену, возвращающий одинаковые ответы тысячам пользователей, запускает ваш Worker каждый раз.
Приведённый ниже шаблон позволяет аутентифицировать каждый запрос и при этом отдавать попадания в кеш без запуска кешируемого обработчика:
- Внешняя (используемая по умолчанию) точка входа получает запрос и выполняет его аутентификацию.
- В случае успеха она удаляет
Authorizationзаголовок и перенаправляет запрос в именованную точку входа черезctx.exports. - Workers Caching располагается перед именованной точкой входа. При попадании в кеш ответ возвращается внешней точке входа, а та передаёт его клиенту: именованная точка входа при этом вообще не запускается.
Отключите кеширование для entrypoint по умолчанию, чтобы аутентификация выполнялась при каждом запросе, но оставьте кеширование включённым для 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() };
}Несколько моментов, на которые стоит обратить внимание:
- Кэш находится в нужном месте. Он располагается между внешней точкой входа и кешируемой точкой входа, поэтому при попадании в кеш дорогостоящая обработка полностью пропускается. Выполняется только проверка авторизации.
Authorizationудаляется перед пересылкой. Именно это делает ответ кешируемым: правило обхода (bypass rule) Cloudflare срабатывает на входящем запросе, а не на ответе, поэтому удаление заголовка до того, как запрос достигнет кешируемой точки входа, позволяет кешируемой точке входаCache-Control: publicвступят в силу. Это также не позволяет токенам участвовать в формировании будущих ключей кэша.- Кэшированный ответ является общим для всех пользователей. Все вызывающие клиенты, прошедшие проверку авторизации, получают одно и то же кешированное тело ответа.
Аутентифицированные ответы для каждого пользователя
Если ваш эндпоинт возвращает данные, специфичные для пользователя, передавайте идентификатор пользователя через ctx.props. Workers Caching включает ctx.props в ключе кеша, поэтому каждый пользователь получает собственную запись кеша, и один пользователь никогда не получит закешированный ответ другого пользователя. Здесь используется та же конфигурация Wrangler, что и в предыдущем примере: кеширование отключено на default, включено на 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() };
}Подробнее об изоляции кеша между вызывающими сторонами см. в Безопасность мультиарендной среды с ctx.props.
Суть этого примера в том, что внешняя точка входа преобразует значение (личность пользователя) в ключ кеша, передавая его через ctx.props : имеет ту же форму, что и в следующем примере, где она влияет на другую часть ключа.
Нормализовать Accept-Encoding для Vary
Vary позволяет кешировать несколько представлений по одному URL: например, вариант ресурса в кодировке Brotli и вариант в кодировке gzip. Cloudflare различает варианты по дословное значение каждого Vary-перечисленным заголовком запроса, поэтому два запроса с семантически эквивалентными, но текстуально разными Accept-Encoding заголовков создают два отдельных варианта.
Для запросов, проходящих через периметр сети Cloudflare, это особенно важно: Accept-Encoding заголовок запроса, который видит ваш Worker, обычно уже переписан Cloudflare в каноническое значение (например, gzip, br) для эффективности кеширования. Исходное значение сохраняется в request.cf.clientAcceptEncoding, но если ваш Worker варьируется по Accept-Encoding без восстановления исходного значения клиента, каждый кэшированный вариант оказывается привязан по ключу к переписанной строке, поэтому кэш отдает вариант Brotli клиентам, которые принимают только gzip, или наоборот.
Решением служит точка входа шлюза, которая восстанавливает Accept-Encoding от request.cf.clientAcceptEncoding перед перенаправлением на кешируемую точку входа. Отключите кеширование на шлюзе и включите его на 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" };
}На что стоит обратить внимание:
- Шлюз запускается на каждый запрос, но он небольшой. Он восстанавливает только один заголовок и вызывает
ctx.exports. Затратные операции (выбор кодировки, загрузка ресурса) выполняются только при промахах кэша. - У вариантов общий идентификатор очистки. Очистка кеша по тегу или префиксу пути делает недействительными сразу все варианты URL, поэтому все варианты должны использовать одинаковый
Cache-Tagзначения. См. примечания в Согласование содержимого с использованиемVary. - Тот же шаблон применяется к другим нормализуемым заголовкам. Если вы хотите варьировать кеш по
Accept-Languageи вы получаете от браузеров длинное сложное значение, нормализуйте его на шлюзе (например, сверните до основного языкового тега) перед пересылкой. Это ограничивает разрастание кеша.
Если варианты для каждой кодировки вам не нужны (например, если Worker всегда возвращает Brotli, когда клиент его поддерживает, а иначе использует gzip), вам не нужен Vary вообще. Выберите каноническую кодировку внутри закешированной точки входа на основе восстановленного Accept-Encoding, и позволить кэшу хранить единственный вариант. См. Accept-Encoding и Content-Encoding для этого варианта шаблона.
До сих пор внутренняя точка входа была функцией от запроса. В следующем примере компонент с состоянием, Durable Object, помещается за тот же этап кэширования с той же структурой.
Кеширование ответов Durable Object
Durable Objects никогда не кешируются напрямую через Workers Caching: они хранят состояние, и кеширование их ответов лишило бы кеш смысла. Но многие endpoints Durable Object обслуживают трафик с преобладанием чтения, для которого короткий TTL кеша вполне допустим: таблицы лидеров, счётчики, агрегированная статистика, конфигурация, которая меняется несколько раз в час.
Такие ответы можно кешировать, обернув Durable Object именованной точкой входа и разместив Workers Caching перед этой точкой входа. При попадании в кеш обёртка не запускается, а Durable Object не затрагивается. Отключите кеширование для точки входа по умолчанию (router) и включите его для CachedLeaderboard обёртку: сам Durable Object никогда не кешируется и не требует настройки кеша:
{
"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>;Почему это работает:
- При попадании в кеш операции чтения ничего не стоят. Workers Caching располагается перед
CachedLeaderboard, поэтому попадание в кеш возвращает закешированное тело без вызова обёртки, без вызова Durable Object и без выполнения дорогостоящей агрегации. Точка входа по умолчанию всё равно запускается для маршрутизации запроса, но она представляет собой лишь тонкий маршрутизатор. - Операции записи немедленно инвалидируют кэш. Обработчик POST обновляет Durable Object, а затем вызывает
ctx.exports.CachedLeaderboard.invalidate(), который запускаетpurge({ tags: ["leaderboard"] })внутриCachedLeaderboard. Это важно, потому что очистки кэша ограничены точкой входа, которая их вызывает : кеш шлюза отключён, поэтому очистка, инициированная со шлюза, не затронет записиCachedLeaderboardсохранён. Самый первый следующий GET-запрос не находит ответ в кэше, повторно выполняет wrapper и сохраняет новый ответ. - Кэшируемая точка входа отвечает за контракт кэширования. Все заголовки cache-control задаются в
CachedLeaderboard, включаяCache-Tag, а такжеCachedLeaderboardтакже предоставляетinvalidate()метод для их очистки. Durable Object при этом ничего не знает о кэшировании.
Если у вас много независимых экземпляров Durable Object (например, по одному на каждого клиента), передавайте идентификатор клиента через ctx.props при вызове кешированной точки входа, так же как Аутентифицированные ответы для каждого пользователя да. Каждый арендатор получает собственную запись в кеше, и очистка кеша для одного арендатора не затрагивает записи других.
Кеширование исходного сервера, который вы не контролируете
Иногда источник, от которого вы зависите, вам не принадлежит. Сторонний API, конечная точка SaaS, общедоступный набор данных, сервис поставщика за медленным CDN: его заголовки кеширования такие, какими их решил сделать владелец, и изменить их вы не можете. Возможно, он отправляет Cache-Control: no-store на всякий случай. Возможно, он вообще ничего не отправляет. Возможно, он агрессивно кеширует данные способом, не соответствующим паттернам чтения вашего приложения. В любом случае вы платите задержкой и стоимостью запроса при каждом вызове.
Workers Caching позволяет разместить перед источником собственный слой кеширования, не меняя ничего на стороне источника. Здесь используется та же схема «внешний плюс внутренний», что и на остальных страницах: тонкая точка входа, которая перенаправляет запросы к источнику, а Workers Caching располагается перед ней и применяет Cache-Control директивы по вашему выбору. Источник сохраняет собственный контракт кеширования с остальным миром: ваш Worker просто добавляет второй, управляемый вами уровень между вашим приложением и этим источником. Как и в других сценариях, отключите кеширование на шлюзе и включите его на 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;Что здесь происходит:
- Слой кэширования принадлежит вам. источника
Cache-Controlзаменяется до того, как ответ попадает в Workers Caching, поэтому TTL, директивы свежести иCache-Tagпространства имён полностью управляются вашим кодом. Вы сами решаете, когда кеш сохраняет ответ, и когда очищать его черезctx.cache.purge(). - Собственная модель кеширования источника остаётся без изменений. Только ваш Worker видит переписанный
Cache-Control. Источник (origin) по-прежнему обслуживает остальных клиентов согласно опубликованным правилам кэширования: вы не изменили ни его поведение, ни модель безопасности, а лишь добавили слой перед ним для своего приложения. - Попадания в кеш никогда не доходят до исходного сервера. Workers Caching располагается перед
CachedOrigin, поэтому попадание в кеш возвращает сохранённый ответ без вызоваfetchс вышестоящим сервером. Именно это снижает объем запросов к источнику и задержку каждого кешированного вызова.
Несколько распространенных расширений этого паттерна:
- TTL для каждого ресурса. Если разным путям на источнике должна соответствовать разная свежесть кеша, используйте ветвление по
url.pathnameвнутриCachedOriginи задать другойmax-age(и другойCache-Tag) для каждого. Ключ кеша уже включает путь и строку запроса, поэтому каждый ресурс получает отдельную запись. - Кэширование для каждого пользователя. Если ваше приложение аутентифицирует вызывающую сторону, а источник возвращает данные, специфичные для пользователя, выполняйте аутентификацию во внешней точке входа и передавайте идентификатор пользователя через
ctx.propsкCachedOrigin: та же структура, что и у Аутентифицированные ответы для каждого пользователя. У каждого пользователя своя запись в кэше, и один пользователь никогда не получит кэшированный ответ другого пользователя. - Stale-while-revalidate. Если источник работает медленно или нестабильно, задайте
Cache-Control: public, max-age=60, stale-while-revalidate=600для кешированного ответа. Большинство запросов сразу возвращают кешированное тело, а Workers Caching обновляет данные с источника в фоновом режиме. Обратитесь к Используйтеstale-while-revalidateдля обновлений с низкой задержкой. - Точечная инвалидация. Помечайте ответы
Cache-Tagзначения, отражающие модель данных вашего приложения (например,Cache-Tag: origin:example, product:42). Когда известно, что источник данных изменился (сработал вебхук, было выполнено действие администратора), вызовитеctx.cache.purge({ tags: ["product:42"] })и следующий запрос заново заполняет кеш.
Это тот же базовый блок, что и в остальных примерах на этой странице. Разница лишь в том, что «затратная операция», которую выполняет кешируемая точка входа при промахе, представляет собой fetch на чужой сервер. Контроль над тем, как долго живет этот ответ, как формируется его ключ и когда он становится недействительным, полностью остаётся на стороне вашего Worker.
Комбинирование паттернов
Все четыре примера показывают одну и ту же архитектуру с четырёх разных точек зрения:
| Внешняя точка входа | Что делает этап кеширования | Внутренняя точка входа |
|---|---|---|
| Аутентифицировать запрос | Кеширование ресурсоемких вычислений для каждого пользователя | Загружает или вычисляет данные пользователя |
Восстановить Accept-Encoding |
Кеширование одного варианта для каждой реальной кодировки | Загружает ресурс с корректной кодировкой |
| Считывание и запись маршрутов | Кеширование чтений с их инвалидацией при записи | Оборачивает Durable Object в Cache-Tag |
| Передать запрос без изменений | Кеширование стороннего источника на ваших условиях | Получает данные с исходного сервера и накладывает поверх Cache-Control |
Между строками меняется только то, что делает внешняя точка входа до вызова и что делает внутренняя точка входа при промахе кеша. Этап кеширования в середине всегда один и тот же примитив: ключ формируется из внутренней точки входа, пути запроса, строки запроса и ctx.props; настраивается свойством внутренней точки входа Cache-Control и Cache-Tag; аннулируется ctx.cache.purge() из той точки входа, которой принадлежат данные.
Именно эта однородность позволяет комбинировать паттерны. Ничто не мешает объединить их в одном Worker:
- Внешняя точка входа, которая выполняет аутентификацию и маршрутизацию.
- Точка нормализации, которая удаляет отслеживающие query-параметры и восстанавливает
Accept-Encoding, и приводит запрос к каноническому виду. - Кэшированная точка входа, которая находится перед Durable Object и помечена для очистки.
- Отдельная кэшируемая точка входа для публичного эндпоинта без аутентификации, также доступная через тот же внешний entrypoint, со своим собственным cache key и
Cache-Tagпространство имён.
Каждый вызов между этими точками входа проходит через собственный этап кэширования. Цепочка построена из одних и тех же трёх строительных блоков: WorkerEntrypoint, ctx.exports, а также Cache-Control заголовок, при этом кеш является этапом цепочки, а не отдельной системой, пристроенной сбоку. Всё, что раньше настраивалось в движке правил кеширования, теперь описывается в виде кода: какой entrypoint выполняется, какой запрос пересылается, какие props передаются, какой Cache-Control возвращается, что именно очищается из кеша.
Фиксированного списка шаблонов не существует. Workers Caching предоставляет кеш перед каждой точкой входа Worker, а как вы им воспользуетесь, зависит только от вас.