← Cloudflare Workers / workers / cache
Ключи кеша
Каждый кешированный ответ сохраняется под ключ кеша. Когда поступает запрос, Cloudflare вычисляет для него ключ кеша и выполняет поиск по нему: при совпадении возвращается сохранённый ответ, при отсутствии совпадения запускается ваш Worker, и его ответ сохраняется под этим ключом для следующего раза.
Два запроса с одинаковым ключом кеша используют один и тот же кешированный ответ. Два запроса с разными ключами кеша получают независимые записи в кеше.
На этой странице объясняется, что Workers Caching включает в ключ кеша, зачем нужен каждый компонент и как учитывать это при проектировании вашего Worker.
Что входит в ключ кеша
Workers Caching кеширует ответы по следующим ключам:
- целевая точка входа : какой именно именованная точка входа Worker получил запрос.
defaultexport и экспортированный класс являются разными точками входа и не используют общий кэш, даже если они формируют идентичные ответы. - путь и строка запроса URL запроса. Порядок параметров запроса имеет значение:
?a=1&b=2и?b=2&a=1это разные ключи кеша. Завершающие слэши тоже имеют значение. - Версия Worker, по умолчанию. У каждой развёрнутой версии есть собственный кэш, поэтому новое развёртывание не отдаёт ответы, записанные предыдущей версией. Отключить это можно с помощью
cache.cross_version_cacheчтобы совместно использовать кешированные ответы для разных версий. См. Аннулирование кэша между развёртываниями. - Вызова
ctx.props, когда Worker вызывается через service binding или RPC. См. Безопасность мультиарендной среды сctx.props.
В качестве меры защиты от отравления кеша ключ также включает:
-
x-http-method-override,x-http-method, а такжеx-method-overrideзаголовки запроса. -
x-forwarded-host,x-host,x-forwarded-scheme(если только его значение неhttpилиhttps),x-original-url,x-rewrite-url, а такжеforwardedзаголовки запроса. - Значение
Cloudflare-Workers-Version-Keyзаголовок запроса. Этот заголовок не устанавливается Cloudflare автоматически: он имеет смысл только тогда, когда вызывающая сторона (например, вышестоящий Worker или прокси) сама решает включить его, чтобы явно разделить кеш дополнительно. Это не зависит от автоматического формирования ключа по версии, описанного выше, которое контролируетсяcache.cross_version_cache.
Об этих трёх пунктах обычно не нужно задумываться. Некоторые фреймворки интерпретируют заголовки переопределения метода и перезаписи URL как замену фактического метода или URL запроса, что может привести к отравление кеша ↗ если два запроса отличаются только этими заголовками, но дают существенно разные ответы. Включение их в ключ кеша гарантирует, что отравленная запись затронет только запросы с тем же отравленным заголовком.
Запросы, которые отличаются только заголовками, не входящими в ключ кеша (например, User-Agent, Accept-Language, Cookie, или Authorization) возвращают один и тот же закешированный ответ. Обычно это и требуется: не нужно, чтобы каждая строка user agent или языковое предпочтение создавали отдельную запись в кеше. Если согласование содержимого всё же нужно, задайте Vary к ответу, либо обработать его внутри своего Worker и формировать канонический ответ для каждого URL.
Примечательно, что ключ кеша не включают:
- HTTP-метод.
GETиHEADзапросы к одному и тому же URL используют одну запись кеша.HEADзапрос может быть обслужен изGETзаполнение (Cloudflare возвращает кешированные заголовки без тела). В обратном случаеHEADзапрос при холодном кеше преобразуется вGETвнутренне, чтобы был получен и сохранён весь ресурс целиком, а последующийGETзатем обращается к записи, котораяHEADзаполнено. (POST,PUT,PATCH, а такжеDELETEвообще никогда не кешируются, поэтому для них этот вопрос не возникает.) - Хост запроса. Кеш Worker использует в качестве ключа путь и строку запроса, а не полный URL. См. Кэш принадлежит Worker, а не домену.
- Тело запроса. Поскольку только
GETиHEADкешируются, это редко имеет значение, но стоит отметить, если ваш Worker читаетrequest.bodyдля кешируемого метода тело не разбивает кеш на части.
На момент запуска нельзя посмотреть точный ключ кеша, который Cloudflare вычислил для запроса. Основными сигналами для понимания поведения кеша служат Cf-Cache-Status заголовок ответа и информацию о попаданиях в кеш для каждого вызова в Панель наблюдаемости Workers. См. Просмотр ключа кеша.
Кэш принадлежит Worker, а не домену
Worker представляет собой сущность без зоны. Его можно вызвать несколькими различными способами:
- Напрямую на
workers.devподдомен. - С помощью маршрут для любой зоны, которой вы управляете.
- С помощью пользовательский домен : и вы можете привязать один и тот же Worker к нескольким пользовательским доменам.
- С помощью привязка к сервису от другого Worker с произвольным именем хоста-заполнителя в URL.
Workers Caching считает все это одним и тем же Worker и использует для них общий кеш. Ключ кеша не включает хост, поэтому запрос к /api/users/42 обращается к одной и той же записи кеша независимо от того, поступил ли он через api.example.com, api.example.net, service binding или workers.dev URL.
Именно такое поведение нужно почти всегда. Ответы Worker зависят от его кода и входных данных, а не от того, на какой домен пришел запрос, поэтому кеширование ответа один раз и последующая отдача его по всем путям входа максимизирует долю попаданий в кеш, не нарушая корректность.
Если вам действительно нужны разные закешированные ответы для одного и того же пути на разных хостах (например, для клиентов на white label, где tenant-a.example.com/index и tenant-b.example.com/index должны выдавать разный контент: сам по себе ключ кеша этого не обеспечивает. Вместо этого различайте арендаторов на уровне вашего gateway Worker и передавайте идентификатор арендатора через ctx.props, который является часть ключа кеша.
Аннулирование кэша между развёртываниями
По умолчанию текущая вызываемая версия Worker является часть ключа кеша. У каждой развёрнутой версии свой кеш, поэтому:
- Новое развёртывание всегда стартует с пустого кэша и никогда не отдаёт ответы, записанные предыдущей версией.
- Изменения, влияющие на кеш, вступают в силу сразу после публикации новой версии: очищать кеш вручную, чтобы перестать отдавать старый контент, не нужно.
- Во время постепенное развёртывание, старая и новая версии наполняют независимые кеши, поэтому часть трафика, идущая на новую версию, никогда не получает ответы старой версии.
Это поведение используется по умолчанию, потому что его проще всего понять. Недостаток в том, что коэффициент попаданий в кеш сбрасывается при каждом развёртывании : первые запросы к новой версии не попадают в кеш, пока он заполняется. Это самая частая причина падения доли попаданий в кеш Worker сразу после деплоя.
Общий доступ к кэшу между версиями
Если вы часто выполняете развертывание, а ответы между развертываниями почти не меняются, сбрасывать прогретый кеш при каждом развертывании нерационально. Задайте cache.cross_version_cache к true чтобы исключить версию из ключа кеша и использовать общие кешированные ответы для всех версий. Ответ, записанный версией A, будет по-прежнему отдаваться после развертывания версии B, пока не истечёт его TTL.
Это максимизирует долю попаданий в кеш ценой более медленного развертывания изменений: поскольку деплой больше не сбрасывает кеш, изменение, влияющее на содержимое ответа, не применится к уже закешированным записям, пока они не истекут или вы не очистите их вручную. Если у вас cross_version_cache включено, и вам нужно, чтобы деплой вступил в силу немедленно, используйте один из двух инструментов ниже.
Помечайте ответы версией и очищайте тег при откате
Если вам нужен точечный контроль, помечайте каждый кэшированный ответ версией Worker, которая его создала. Позже очистка кэша по этой метке версии удалит все записи, созданные этой версией, не затронув кэшированные ответы других версий.
Здесь используется привязка метаданных версии чтобы прочитать текущий идентификатор версии во время запроса и добавить его в начале в качестве Cache-Tag значение. См. Очистка кэша для конкретной версии для полного шаблона с кодом.
Это лучший вариант, если у вас включен cross_version_cache и вам может потребоваться откатиться к определённой версии, не уничтожая кешированное содержимое рабочих версий.
Полная очистка кеша после развёртывания
Более простой подход: после каждого деплоя отправляйте запрос из CI на небольшой эндпоинт Worker, который вызывает ctx.cache.purge({ purgeEverything: true }). Следующий запрос после очистки кэша заново наполняет кэш той версией Worker, которая на тот момент опубликована.
Этот способ грубее, но не требует логики внутри Worker. Используйте его, если у вас включен cross_version_cache но при этом хотите, чтобы отдельные развертывания сбрасывали кеш. При использовании кеша по умолчанию для каждой версии развертывания и так начинаются с холодного кеша, поэтому в этом нет необходимости.
Безопасность мультиарендной среды с ctx.props
Когда ваш Worker вызывается через привязка к сервису или RPC, вызывающей стороны ctx.props является частью ключа кеша. Два вызывающих объекта, обращающихся к вашему Worker с разными ctx.props получить отдельные записи кэша : один вызывающий никогда не получит закешированный ответ другого вызывающего.
Именно этот механизм обеспечивает безопасность кеширования для мультитенантных Workers, вызываемых через service binding. Если вы используете ctx.props чтобы передавать контекст авторизации конкретного вызывающего: ID пользователя, ID тенанта, организацию, роль. В этом случае кеширование по умолчанию безопасно: ответы, логически относящиеся к одному вызывающему, не могут через кеш попасть к другому.
import { WorkerEntrypoint } from "cloudflare:workers";
export default class Backend extends WorkerEntrypoint {
async fetch(request) {
// ctx.props.userId is set by the caller (for example, an auth gateway).
// Because it is part of the cache key, User A and User B requesting the
// same URL get separate cache entries — there is no way for one to
// see the other's response.
const { userId } = this.ctx.props;
const data = { userId, timestamp: Date.now() };
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
},
});
}
}import { WorkerEntrypoint } from "cloudflare:workers";
interface Props {
userId: string;
}
export default class Backend extends WorkerEntrypoint<Env, Props> {
async fetch(request: Request): Promise<Response> {
// ctx.props.userId is set by the caller (for example, an auth gateway).
// Because it is part of the cache key, User A and User B requesting the
// same URL get separate cache entries — there is no way for one to
// see the other's response.
const { userId } = this.ctx.props;
const data = { userId, timestamp: Date.now() };
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
},
});
}
}URL для Service binding
Вызовы Service binding заслуживают отдельного замечания, поскольку передаваемый URL означает не то, что вы могли бы подумать.
При вызове service binding с fetch(), хост в URL это лишь заглушка. Запрос маршрутизируется через привязку, а не через DNS, поэтому хост никогда не разрешается. А поскольку хост не входит в ключ кеша (как описано в Кэш принадлежит Worker, а не домену), заполнитель также не влияет на кеширование. Только путь (и строка запроса) участвуют в формировании ключа кеша наряду с целевой точкой входа и ctx.props:
export default {
async fetch(request, env, ctx) {
// "internal" here is just a placeholder — it is not routed anywhere
// and is not part of the cache key.
//
// What identifies this cached response is:
// - the BACKEND entrypoint
// - the path "/api/users/42"
// - whatever ctx.props the gateway passes along
return env.BACKEND.fetch("http://internal/api/users/42");
},
};interface Env {
BACKEND: Fetcher;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
// "internal" here is just a placeholder — it is not routed anywhere
// and is not part of the cache key.
//
// What identifies this cached response is:
// - the BACKEND entrypoint
// - the path "/api/users/42"
// - whatever ctx.props the gateway passes along
return env.BACKEND.fetch("http://internal/api/users/42");
},
} satisfies ExportedHandler<Env>;Если вы хотите, чтобы кэшированные ответы отличались для разных вызывающих сторон, варьируйте ctx.props. Если нужно, чтобы они различались в зависимости от запроса, варьируйте путь или строку запроса. Изменение имени хоста ни на что не влияет.
Просмотр ключа кеша
На момент запуска о поведении кеша можно судить по двум сигналам:
-
Cf-Cache-Statusзаголовок ответа. Чаще всего вы будете видеть значенияHIT,MISS,EXPIRED,REVALIDATED,UPDATING,STALE, а такжеBYPASS.HITозначает, что Cloudflare вернул закешированный ответ, не запуская ваш Worker.MISSозначает, что ваш Worker выполнился и ответ был сохранён.UPDATINGозначает, что закешированный ответ устарел, и ваш Worker выполнялся в фоновом режиме, чтобы обновить его.BYPASSозначает, что кеширование для этого запроса было отключено. См. Кеширование ответов Cloudflare для полного набора значений. -
Попадания в кеш в Панель наблюдаемости Workers. Каждый вызов показывает, был ли ответ получен из кеша, поэтому вы можете фильтровать и агрегировать данные о попаданиях в кеш по всему трафику вашего Worker.
Cloudflare пока не раскрывает саму структуру ключа кэша. Если два запроса, которые должны были использовать общий кэшированный ответ, этого не делают, вам придётся определить, какая часть ключа отличалась, ориентируясь на компоненты, перечисленные в Что входит в ключ кеша. Обзор типичных проблем кэширования и способов их диагностики см. в Отладка.
Custom cache keys
По умолчанию путь и строка запроса URL-адреса запроса формируют компонент URL в ключе кеша. Когда одна точка входа вызывает другую кешируемую точку входа через ctx.exports loopback, вызывающая точка входа может переопределить этот компонент, задав cf.cacheKey к запросу.
В примере ниже Backend точка входа является кешированной. Точка входа по умолчанию перенаправляет запросы к ней через ctx.exports, выбрав сам ключ кэша:
import { WorkerEntrypoint } from "cloudflare:workers";
// Cached entrypoint. Requests routed here through ctx.exports are served
// from cache when possible.
export class Backend extends WorkerEntrypoint {
async fetch(request) {
return new Response("Hello from the backend", {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
},
});
}
}
// Gateway entrypoint. Calls the cached Backend entrypoint via ctx.exports,
// which routes through the cache, and chooses the cache key for the call.
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Strip a tracking parameter so that requests differing only by
// `utm_source` resolve to the same cached entry.
url.searchParams.delete("utm_source");
return ctx.exports.Backend.fetch(request, {
cf: { cacheKey: url.pathname + url.search },
});
},
};import { WorkerEntrypoint } from "cloudflare:workers";
// Cached entrypoint. Requests routed here through ctx.exports are served
// from cache when possible.
export class Backend extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
return new Response("Hello from the backend", {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
},
});
}
}
// Gateway entrypoint. Calls the cached Backend entrypoint via ctx.exports,
// which routes through the cache, and chooses the cache key for the call.
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
// Strip a tracking parameter so that requests differing only by
// `utm_source` resolve to the same cached entry.
url.searchParams.delete("utm_source");
return ctx.exports.Backend.fetch(request, {
cf: { cacheKey: url.pathname + url.search },
});
},
} satisfies ExportedHandler<Env>;Пользовательский ключ кэша заменяет путь и строку запроса в ключе кеша. Всё остальное, описанное в Что входит в ключ кеша по-прежнему применяется:
- Целевая точка входа и вызывающего объекта
ctx.propsостаются частью ключа. Пользовательский ключ кеша не может охватывать несколько точек входа или несколькоctx.props, поэтому изоляция арендаторов описанное выше, остается верным, даже если вызывающие стороны выбирают собственные ключи. Пользовательский ключ всегда обращается только к записям в пределах собственного пространства кеша вызываемой стороны. - Два запроса с разных URL-адресах, но с одинаковым
cf.cacheKeyприводят к одной и той же записи кеша. Именно так несколько URL объединяются в один закешированный ответ. - Два запроса с тот же URL, но разные
cf.cacheKeyприводят к разным записям кеша.
Задайте cf.cacheKey в пустую строку либо не указывайте его, чтобы использовать ключ по умолчанию, производный от URL.
В этом шаблоне точка входа по умолчанию выступает шлюзом, который должен выполняться при каждом запросе, поэтому отключите для неё кеширование и оставьте его включённым для Backend (см. Кэширование для каждой точки входа):
{
"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 } },
"Backend": { "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.Backend]
type = "worker"
[exports.Backend.cache]
enabled = trueЧто можно сделать с помощью пользовательского ключа кеша
- Игнорировать части URL. Удалите параметры отслеживания (
utm_source,gclid), либо полностью отбрасывайте строку запроса, чтобы варианты, не влияющие на ответ, использовали одну запись кеша. - Формируйте ключ кеша не только по URL. Формируйте ключ на основе значения, которому доверяет ваш Worker-шлюз, например нормализованного идентификатора ресурса, чтобы несколько эквивалентных URL сопоставлялись с одной записью.
- Разделите кэш самостоятельно. Добавьте к ключу различающее значение (например, версию содержимого), чтобы принудительно создавать отдельные записи для запросов, которые иначе совпали бы.
Для изоляции на уровне вызывающей стороны продолжайте использовать ctx.props а не кодирования личности вызывающей стороны в ключе кеша: ctx.props автоматически становится частью ключа, и это нельзя обойти.
Пользовательские ключи применяются только к вызовам в пределах одного аккаунта
cf.cacheKey учитывается только тогда, когда вызов остается в пределах вашего аккаунта. Cloudflare отбрасывает cf объект при каждом запросе, который пересекает границу аккаунта, например при service binding к Worker, принадлежащему другому аккаунту. В этом случае пользовательский ключ игнорируется, и ключ кеша возвращается к URL запроса, поэтому вызывающая сторона в одном аккаунте никогда не сможет повлиять на кеш Worker в другом аккаунте (или проверить его).
Это также означает cf.cacheKey не действует на запросы конечных пользователей. cf объект во входящем запросе от браузера или API клиента заполняется Cloudflare, а не клиентом, поэтому клиент не может задать собственный ключ кеша.