Ускорьте работу вашего Worker: собственный кэш на входе

Ниже представлен перевод HTML-контента с английского на русский язык. Все HTML-теги сохранены в точности, атрибуты тегов не изменены.

Сегодня мы запускаем Workers Cacheмногоуровневый кеш, который располагается перед вашим Worker’ом и настраивается одной строкой конфигурации Wrangler с использованием тех же заголовков Cache-Control, которые вы уже знаете.

Когда Workers Cache включён, каждый кешируемый запрос к вашему Worker’у сначала попадает в кеш Cloudflare. Если в кеше есть свежий ответ, Cloudflare возвращает его напрямую — Worker не запускается, и вы не платите за процессорное время. В случае промаха Worker выполняется, и если ваш ответ подлежит кешированию, Cloudflare сохраняет его для следующего запроса. Следующий запрос из любой точки Земли может быть обслужен прямо из кеша.

Ускорьте работу вашего Worker: собственный кэш на входе

Всё это описывается одним блоком конфигурации:

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": {
    "enabled": true
  }
}

После этого вы управляете кешированием так, как HTTP всегда и предполагал — устанавливая заголовки в ваших ответах:

return new Response(body, {
  headers: {
    "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
    "Cache-Tag": "products,product:123",
  },
});

А когда контент меняется, ваш Worker очищает собственный кеш:

await ctx.cache.purge({ tags: ["product:123"] });

Это и есть весь API. Не нужно настраивать зону, подключать механизм правил, выделять отдельный кеш или заходить во второй продукт. Код Worker’а — это поверхность конфигурации, а кеш следует за Worker’ом везде, где он выполняется: на пользовательском домене, на workers.dev, за привязкой служб, в предпросмотре, в арендаторе Workers for Platforms. Один Worker, один кеш, одна настройка.

Вот и вся поверхность. Под ней скрыто многое: многоуровневое кеширование по всей нашей сети, полная поддержка stale-while-revalidate, благодаря которому устаревшие ответы никогда не блокируют пользователя, согласование контента через Vary, многопользовательские ключи кеша через ctx.props, программная очистка по тегам или префиксу пути, а также — то, что мы считаем самым большим открытием — кеш, который стоит перед каждой точкой входа Worker’а, а не только публичной, с возможностью управления для каждой точки входа: какие кешировать, а какие нет. Последняя часть означает, что вы можете встраивать кеширование напрямую в структуру вашего приложения: цепочка точек входа с этапами кеширования, размещёнными там, где они нужны, и настраиваемыми кодом с обеих сторон. Мы подробно рассмотрим всё это ниже.

Workers Cache доступен сегодня для каждого Worker’а на любом тарифном плане и включается в Wrangler.

Это API кеширования, который мы всегда хотели дать Workers. Вот почему нам потребовалось так много времени, что становится возможным благодаря этому, и что будет дальше.

Почему серверно-рендеренговые приложения нуждаются в кеше перед собой

Когда мы представили Workers в 2017 году, идея заключалась в том, что вы можете запускать код в сети Cloudflare для преобразования запросов на пути к вашему источнику. Worker располагался перед кешем и источником:

Ускорьте работу вашего Worker: собственный кэш на входе

Это была правильная модель для тех сценариев использования, на которые мы ориентировались. Если вы хотели добавить заголовок к каждому запросу, переписать URL, провести A/B-тест или отфильтровать трафик до того, как он достигнет источника, размещение Worker’а перед кешем и источником давало вам полный контроль над тем, что кешируется, а что нет. Клиенты создавали с этим невероятные вещи.

Но мир изменился. Workers перестали быть чем-то, что вы прикрепляете к источнику, и стали самим источником. Фреймворки, такие как Astro, TanStack Start, Next.js, Remix и SvelteKit, — все они поставляют адаптер Cloudflare, который собирает ваше приложение как Worker. За ними нет источника. Worker и есть сервер.

Когда Worker является источником, в первоначальной архитектуре нечего кешировать. Каждый запрос выполняет ваш код, даже если ответ байт в байт совпадает с тем, который вы вернули секунду назад. Среда выполнения Workers достаточно быстра, чтобы это работало — она без труда обрабатывает десятки миллионов запросов в секунду, — но «достаточно быстра для обработки каждого запроса» всё равно означает задержку на каждой загрузке страницы и процессорное время на каждом вызове. А в серверно-рендеренговом приложении каждая загрузка страницы по определению является рендерингом.

Workers Cache переворачивает архитектуру. Теперь кеш Cloudflare располагается перед Worker’ом:

Ускорьте работу вашего Worker: собственный кэш на входе

При попадании в кеш ваш Worker вообще не запускается. Cloudflare возвращает кешированный ответ, и ваше процессорное время остаётся нулевым. При промахе Worker выполняется один раз, заполняет кеш, и следующий запрос — откуда угодно — обслуживается из кеша без вызова вашего кода.

Именно этого не хватало для серверного рендеринга на Workers. Раньше приходилось выбирать между двумя неоптимальными вариантами:

  • Предварительный рендеринг всего на этапе сборки («генерация статического сайта»). Быстрая загрузка страниц, но любое изменение требует полной пересборки и развёртывания. Для сайта документации с несколькими тысячами страниц это занимает 5–10 минут. Для крупного интернет-магазина — ещё дольше, и сборка запускается каждый раз, когда вы что-то меняете.

  • Рендеринг каждой страницы при каждом запросе. Актуальный контент, но каждая загрузка страницы оплачивается затратами на рендеринг, и каждый посетитель платит за задержку.

Workers Cache предлагает третий вариант: серверный рендеринг по требованию, кеширование отрендеренного ответа и обновление его с заданным вами временем жизни (TTL). Первый запрос к новой странице всё ещё рендерится. Каждый последующий запрос, пока кеш не истёк, обслуживается так, будто страница статическая. Когда кеш истекает, следующий запрос запускает повторный рендеринг — а с помощью stale-while-revalidate даже этот запрос не ждёт.

Вы получаете скорость статического сайта без времени сборки и свежесть серверного рендеринга без затрат. Никаких специфичных для фреймворка механизмов вроде Incremental Static Regeneration. Просто HTTP-кеширование, работающее так, как было задумано, перед кодом, который был спроектирован как источник.

stale-while-revalidate — это то, что делает работу мгновенной

Директива stale-while-revalidate сообщает Cloudflare, что после истечения срока действия кешированного ответа разрешается немедленно отдать устаревшую копию, пока в фоновом режиме обновляется ответ. Cloudflare полностью реализовал поддержку stale-while-revalidate в начале этого года, и именно эта директива превращает «мы кешируем ваш Worker» в «сайт вашего Worker'а ощущается как статический».

Без неё первый запрос после истечения записи в кеше вынужден ждать, пока Worker выполнит рендеринг страницы с нуля. Пользователь видит эту задержку. С ней первый запрос после истечения получает устаревшую страницу немедленно (с заголовком Cf-Cache-Status: UPDATING), а Worker выполняется в фоне, чтобы перезаполнить кеш. Каждый пользователь, включая того, кто вызвал обновление, получает ответ со скоростью кеша.

Ускорьте работу вашего Worker: собственный кэш на входе

На практике это выглядит так:

 export default {
  async fetch(request) {
    const html = await renderPage(request);
    return new Response(html, {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
        // Считается свежим в течение 5 минут; устаревший ответ отдаётся до часа,
        // пока в фоне выполняется обновление.
        "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
      },
    });
  },
};

Мысленная модель, которая помогает это понять:

  • Окно свежести (max-age): Cloudflare отдаёт кешированный ответ. Worker не запускается.

  • Окно устаревания (stale-while-revalidate): Cloudflare отдаёт кешированный ответ. Worker выполняется в фоне для его обновления. Ни один пользователь не ждёт.

  • Вне обоих окон: Cloudflare запускает Worker для генерации свежего ответа, и пользователь ждёт один этот рендеринг.

Вы выбираете окна. Для каталога продуктов, который обновляется каждые несколько минут, max-age=300, stale-while-revalidate=3600 означает, что посетители практически никогда не ждут, а ваш Worker всё ещё запускается достаточно часто, чтобы контент оставался свежим. Для архива блога, который почти никогда не меняется, max-age=86400, stale-while-revalidate=2592000 означает, что ваш Worker запускается один раз в день на страницу.

Первый запрос к совершенно новой странице — единственный, который оплачивает полную стоимость рендеринга. После этого страница ведёт себя как статический вывод для посетителей, в то время как ваш Worker по-прежнему управляет тем, как страница генерируется.

Один URL, множество представлений: работает Vary

Настоящие приложения редко возвращают одинаковые байты каждому клиенту. Одна и та же страница продукта может быть HTML для браузера и JSON для API-клиента. Одно и то же изображение может быть WebP для клиентов, которые его поддерживают, и JPEG для тех, кто нет. Одна и та же домашняя страница может возвращаться на английском, французском или японском языке в зависимости от пользователя.

Сделать это без кэша легко — ваш Worker просто читает заголовок запроса и возвращает правильный результат. Сделать это с кэшем — вот где обычно всё усложняется. Большинство кэшей предлагают два плохих варианта: не кэшировать ничего на URL-адресах с несколькими представлениями или кэшировать одно представление и обслуживать его для всех.

Workers Cache поддерживает стандартный HTTP-заголовок Vary, который является правильным способом решения этой проблемы. Когда ваш Worker возвращает ответ с Vary: Accept-Encoding (или Accept, или Accept-Language, или любой другой заголовок запроса), Cloudflare сохраняет отдельный кэшированный вариант для каждой уникальной комбинации этих заголовков — и возвращает только тот вариант, чьи сохранённые значения соответствуют входящему запросу.

export default {
  async fetch(request) {
    const accept = request.headers.get("Accept") ?? "";
    const wantsWebp = accept.includes("image/webp");

    const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();

    return new Response(body, {
      headers: {
        "Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
        "Cache-Control": "public, max-age=3600",
        // Кэшировать отдельный вариант для каждого уникального значения заголовка Accept.
        Vary: "Accept",
      },
    });
  },
};

Один URL, два кэшированных варианта. Браузер, отправляющий Accept: image/webp,*/*, получает WebP. Браузер, отправляющий Accept: image/jpeg, получает JPEG. Оба приходят из кэша. Ваш Worker записывает оба варианта при первом запросе к каждому из них, а затем не запускается ни разу для любого из них после этого.

Это хорошо известный HTTP-стандарт для согласования контента, и Workers Cache реализует его так, как описано в RFC 9110 и RFC 9111. Нет белого списка заголовков, для которых можно использовать Vary. Вы перечисляете всё, что нужно, и Cloudflare создаёт ключи для вариантов на основе точных значений. В документации рассматриваются пограничные случаи — как контролировать разрастание вариантов путём нормализации заголовков в шлюзовом Worker, почему очистка инвалидирует все варианты URL вместе и один случай (Vary: *), который полностью отключает кэширование.

Это кэш вашего Worker, а не вашей зоны

Прежде чем мы перейдём к тому, что становится возможным благодаря всему этому, стоит упомянуть один концептуальный сдвиг.

У Cloudflare уже давно есть кэш. Он настраивается на уровне зоны: Cache Rules, Page Rules, список расширений кэшируемых файлов, Cache Reserve, топология Tiered Cache, пользовательские ключи кэша. Всё это настраивается для каждой зоны, и исторически Worker должен был либо вписываться в конфигурацию этой зоны, либо обходить её.

Workers Cache отличается. Это кэш вашего Worker — он принадлежит Worker, а не зоне. Это имеет ряд последствий, которые оказываются важными:

  • Нет необходимости управлять конфигурацией зоны. Cache Rules, настройки уровня кэша, список расширений файлов, Page Rules — ни одно из них не применяется к Workers Cache. Заголовки Cache-Control Worker'а и есть конфигурация.

  • Кэш следует за Worker, а не за доменным именем. Worker, привязанный к api.example.com, api.example.net и вызываемый через сервисную привязку, использует один кэш для всех трёх. Запрос к /users/42 попадает в одну и ту же кэшированную запись независимо от того, каким путём он поступил.

  • Кэш работает на workers.dev. Он работает в предварительных URL (каждый предпросмотр имеет свой собственный кэш, так что тестирование изменений не загрязняет продакшн). Он работает в Workers for Platforms (каждый пользовательский Worker имеет свой собственный кэш, изолированный от диспетчера и других арендаторов). Раньше все они были гражданами второго сорта для кэширования. Теперь это не так.

  • Очистка ограничена точкой входа Worker'а. Когда вы вызываете ctx.cache.purge({ purgeEverything: true }), вы очищаете только кэш точки входа вашего Worker'а. Нет риска уничтожить другой контент вашей зоны. Нет риска, что развёртывание одного Worker'а инвалидирует данные другого.

Всё, что вы настраиваете относительно кэширования, вы настраиваете в коде: какие пути получают более длинные TTL (ветвление по пути и установка другого max-age), какие запросы обходят кэш (возврат Cache-Control: private), как формируется ключ кэша (контроль того, что попадает в ctx.props, нормализация URL в шлюзовом Worker перед отправкой). Worker, который вы уже написали, и есть поверхность конфигурации.

Полная документация подробно рассматривает это в Workers Cache: кэш вашего Worker.

Два уровня, каждый Worker, никакой конфигурации

Workers Cache по умолчанию регионально многоуровневый. Есть два слоя:

  • Нижний уровень в центре обработки данных Cloudflare, ближайшем к пользователю. Каждый центр обработки данных, который получает трафик для вашего Worker, имеет свой собственный кэш нижнего уровня.

  • Верхний уровень, который агрегирует заполнения по всей сети. Таких уровней меньше, и каждый нижний уровень обращается к верхнему при промахе.

Запрос сначала попадает на нижний уровень. При попадании ответ возвращается, и на этом всё. При промахе нижний уровень спрашивает верхний. При попадании там ответ возвращается и также сохраняется в нижнем уровне на обратном пути. Только если оба уровня дают промах, ваш Worker действительно запускается — и ответ от этого запуска сохраняется в обоих уровнях.

Ускорьте работу вашего Worker: собственный кэш на входе

Причина, по которой это важно, заключается в том, что первый запрос в любой точке мира заполняет верхний уровень. Каждый последующий запрос из любого центра обработки данных может быть обслужен с верхнего уровня без запуска вашего Worker — даже если нижний уровень в этом центре обработки данных никогда не видел этот запрос ранее. Показатели попадания в кэш значительно выше, чем с одним плоским слоем кэша, что именно то, что нужно, когда ваш Worker является источником.

Это та же топология, которая сегодня используется для Tiered Cache для зон, за исключением того, что вам не нужно её настраивать. Нет диалогового окна "включить многоуровневый кэш для моего Worker". Каждый Worker с включенным кэшированием получает многоуровневость бесплатно.

Если ваш Worker использует Smart Placement, кэш чисто взаимодействует с ним: сначала проверяются уровни, и только если оба дают промах, Smart Placement направляет выполнение ближе к вашему источнику. У нас есть ещё что сказать о том, как эти слои взаимодействуют, включая несколько шероховатостей, которые мы планируем сгладить, в документации.

Запускайте своё приложение рядом с пользователем и рядом с данными

Существует повторяющееся противоречие в веб-производительности, которое никто полностью не разрешил: вы хотите, чтобы ваш код выполнялся близко к пользователю (потому что время кругового пути между пользователем и сервером находится на критическом пути), и вы хотите, чтобы ваш код выполнялся близко к данным (потому что каждый запрос к базе данных также является круговым путём). Выберите одно, и другое станет медленным.

Мы годами стремились к обоим. Наша сеть находится в пределах ~50 мс от примерно 95% интернет-пользователей мира. Smart Placement и Placement Hints позволяют вам держать свой код рядом с данными, не думая о облачных регионах. Но до сих пор эти две части полностью не сочетались. Вы могли сделать "рядом с пользователем" или "рядом с данными", и если вы хотели, чтобы обе половины вашего приложения одновременно находились в правильном месте, вам нужно было быть экспертом Cloudflare. Мы знали, что можем сделать лучше.

Workers Cache — это компонент, который устраняет разрыв. Поскольку кэш принадлежит Worker (а не зоне), и поскольку сервисные привязки и вызовы ctx.exports между Workers проходят через кэш, вы можете построить приложение как цепочку Workers — каждый работает там, где должен, — с кэшем в качестве соединительного шва между ними.

Архитектура выглядит так:

Ускорьте работу вашего Worker: собственный кэш на входе
  • Worker A работает рядом с пользователем. Он обрабатывает дешёвые, критичные к задержкам части каждого запроса: аутентификацию, ограничение скорости, маршрутизацию, нормализацию заголовков, рендеринг внешней "оболочки" HTML-страницы, которая не зависит от данных.

  • Worker B работает рядом с данными, благодаря Smart Placement или явной подсказке размещения (Placement Hint). Он выполняет тяжелую работу: серверный рендеринг страниц, получающих данные, чтение каталогов продуктов, формирование результатов поиска, агрегацию API, дорогостоящие преобразования.

  • Workers Cache находится перед Worker B. Когда Worker A вызывает Worker B через сервисную привязку, Cloudflare сначала проверяет кэш Worker B. При попадании Worker A получает ответ, а Worker B вообще не запускается — никакого перехода в дата-центр, никакого запроса к базе данных, никакой работы по рендерингу.

Путь при попадании в кэш становится: пользователь → Worker A рядом с пользователем → попадание в кэш Worker B → ответ. Переход к данным происходит только при промахе. Ваши горячие страницы работают со скоростью кода перед пользователем, а холодные страницы всё же выигрывают от выполнения рядом с данными, когда они действительно выполняются.

Вам не нужно проектировать ничего особенного, чтобы получить это. Напишите ваше приложение как два Workers, укажите один на другой с помощью сервисной привязки, включите кэширование в файле wrangler.jsonc Worker B, и готово.

Ускорьте работу вашего Worker: собственный кэш на входе

Мультиарендность по умолчанию с ctx.props

Если вы кэшируете Worker, который возвращает данные, специфичные для пользователя — например, API, отдающее разный контент для каждого залогинившегося пользователя — вам нужен способ гарантировать, что один пользователь никогда не увидит кэшированный ответ другого пользователя. Стандартное решение — «не кэшировать аутентифицированные запросы», и автоматический обход Cloudflare для заголовков Authorization делает именно это. Но «не кэшировать ничего» означает отказ от всего выигрыша в производительности.

Workers Cache решает эту проблему, делая ctx.props вызывающего Worker частью ключа кэша. Когда один Worker вызывает другой через сервисную привязку и передаёт ctx.props с ID пользователя, ID арендатора или любым другим идентификатором, вызывающие с разными props получают отдельные записи кэша. Ответ одного пользователя никогда не просочится в кэш другого пользователя.

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 part of the cache key. User A and User B
    // requesting the same URL get separate cached entries.
    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=300",
      },
    });
  }
}

Типичный шаблон: аутентифицировать запрос в gateway Worker, удалить заголовок Authorization, установить ID аутентифицированного пользователя в ctx.props, а затем вызвать кэшируемый backend Worker. Gateway запускается на каждом запросе (должен, чтобы аутентифицировать), но дорогостоящий backend запускается только тогда, когда для этого пользователя ещё нет записи в кэше. Аутентифицированные API переходят из состояния «некэшируемые» в «кэшируемые для каждого пользователя с полной безопасностью», и ключ кэша выполняет изоляцию за вас. Документация подробно рассматривает это в Multitenant safety with ctx.props и в примере Per-user authenticated responses.

Другие CDN заставляют вас выбирать между корректностью и коэффициентом попадания: либо ключи кэша по токену каждого пользователя, либо отправлять каждый запрос обратно на источник для авторизации. Workers Cache позволяет вам разделять кэшированные ответы API на границе (edge), сохраняя при этом границы авторизации для каждого запроса. Мы не знаем другой CDN, которая предлагает это как встроенную модель для аутентифицированных мультиарендных API. Мы этим довольно гордимся.

Кэш между каждой точкой входа Worker

Вот та часть Workers Cache, которая, по нашему мнению, является самым большим открытием, и её труднее всего увидеть, если думать о ней как о «кэше CDN, который просто работает перед Workers».

Workers Cache находится перед каждой точкой входа Worker — экспортом по умолчанию, каждым именованным WorkerEntrypoint и каждым вызовом между точками входа в одном Worker через ctx.exports. Этот последний пункт — тот, который меняет то, что вы можете построить.

Когда одна точка входа вызывает другую через ctx.exports, кэш обрабатывает этот вызов так же, как обрабатывал бы запрос от браузера. Попадание возвращает кэшированный ответ, и вызываемый объект никогда не запускается. Промах запускает вызываемый объект и сохраняет его ответ под собственным ключом кэша — ключом, основанным на точке входа вызываемого объекта, пути, строке запроса и ctx.props. Вызывающий по-прежнему выполняется при каждом запросе, но всё, что он передаёт вызываемому объекту, мемоизируется независимо.

Вы решаете для каждой точки входа, какие из них кэшировать. В конфигурации Wrangler карта exports позволяет вам включать или выключать кэширование для каждой точки входа по имени ("default" — это экспорт по умолчанию). Включите точку входа in, чтобы кэшировать ответы, которые она создаёт; исключите её out, чтобы она выполнялась при каждом запросе. Точка входа шлюза или маршрутизатора — всё, что аутентифицирует, нормализует или распределяет — должна быть исключена, чтобы она всегда выполнялась и её собственный вывод никогда не обслуживался из кэша.

Это даёт вам примитив, который можно компоновать. Вы можете создать Worker как цепочку маленьких точек входа — аутентификация, нормализация, маршрутизация, дорогостоящее чтение, слой данных — и позволить Workers Cache встраиваться туда, куда вы хотите. Каждая кэшируемая точка входа — это единица мемоизации со своим собственным ключом, своим TTL и своим пространством имён тегов для очистки. Всё, что вы хотели бы настроить в отношении кэширования — когда он выполняется, по какому ключу, когда инвалидируется — выражается как обычный код Worker: какую точку входа вы вызываете, какой запрос перенаправляете, какие ctx.props передаёте, какой Cache-Control устанавливаете.

Чтобы сделать это конкретным, вот один Worker, который делает три вещи, которые вы не могли бы легко сделать вместе на любой другой платформе: он аутентифицирует каждый запрос, кэширует дорогостоящий бэкенд за ключом кэша, безопасным для мультиарендности, и инвалидирует этот кэш при изменении данных.

Кэширование настраивается для каждой точки входа. Шлюз должен выполняться при каждом запросе — как для аутентификации, так и потому что кэшированный ответ шлюза пропустил бы эту проверку аутентификации — поэтому мы отключаем кэширование для точки входа по умолчанию и включаем его только для внутренней:

Вот перевод HTML-контента с сохранением всех тегов:
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": { "enabled": true },
  "exports": {
    // The gateway runs on every request — don't cache it.
    "default": { "type": "worker", "cache": { "enabled": false } },
    // Cache the expensive inner entrypoint.
    "CachedBackend": { "type": "worker", "cache": { "enabled": true } }
  }
}
import { WorkerEntrypoint } from "cloudflare:workers";

interface Env { API_TOKEN: string; }
interface Props { userId: string; }

// Inner entrypoint: the expensive work. Workers Cache sits in front
// of this — on a hit, this code never runs.
export class CachedBackend extends WorkerEntrypoint<Env, Props> {
  async fetch(request: Request): Promise<Response> {
    // ctx.props.userId is part of the cache key, so this is cached
    // separately for every user.
    const { userId } = this.ctx.props;
    const data = await loadExpensiveData(userId);

    return new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
        "Cache-Tag": `user:${userId}`,
      },
    });
  }

  // Invalidate a user's cached response. purge() is scoped to the
  // entrypoint that calls it, so it must run inside CachedBackend —
  // the entrypoint that owns the cached response.
  async invalidate(userId: string): Promise<void> {
    await this.ctx.cache.purge({ tags: [`user:${userId}`] });
  }
}

// Outer entrypoint: runs on every request to authenticate and route.
// Caching is disabled for it in Wrangler config (above), so it always
// runs and the auth check is never skipped by a cache hit.
export default {
  async fetch(request, env, ctx): Promise<Response> {
    const userId = await authenticate(request, env);
    if (!userId) return new Response("Unauthorized", { status: 401 });

    // Invalidate this user's cache on writes, from the entrypoint that
    // owns it.
    if (request.method === "POST") {
      await handleWrite(request, userId);
      await ctx.exports.CachedBackend.invalidate(userId);
      return new Response("OK");
    }

    // For reads: strip Authorization (otherwise Cloudflare's automatic
    // bypass fires and nothing caches), then dispatch to the cached
    // backend with the authenticated user's identity in ctx.props.
    const forwarded = new Request(request);
    forwarded.headers.delete("Authorization");

    return ctx.exports.CachedBackend.fetch(forwarded, {
      props: { userId },
    });
  },
} satisfies ExportedHandler<Env>;

Всё это один Worker. Один исходный файл. Один деплой. Но есть два этапа выполнения — кэширование выключено для шлюза (gateway) и включено для бэкенда в одном небольшом блоке exports — и между ними расположен кэш, ключом которого является пользователь, инвалидируемый по пути записи и отдающий устаревшие данные во время фоновых обновлений. Этап кэширования — не то, что вы добавили «сбоку». Это слой программы, написанный в коде.

Паттерны, которые из этого складываются, открыты. Та же форма работает для:

  • Кэширования Durable Object. Оберните Durable Object за точкой входа, установите Cache-Control в ответе, и чтения перестанут обращаться к Durable Object при попадании. Записи идут напрямую в DO и сбрасывают кэш по тегу. DO остаётся в неведении, что происходит кэширование.

  • Нормализации Accept-Encoding перед Vary. Внешняя точка входа восстанавливает исходную кодировку из request.cf.clientAcceptEncoding (Cloudflare на переднем крае нормализует её для эффективности кэширования) и перенаправляет запрос в кэшируемую точку входа, которая варьируется по реальному значению. Коэффициент попаданий остаётся высоким; клиенты получают правильную кодировку.

  • Удаления параметров отслеживания перед кэшированием. Внешняя точка входа канонизирует URL — или устанавливает пользовательский ключ кэша с помощью cf.cacheKey при вызове ctx.exports — так что кэшируемая внутренняя точка входа видит только каноническую форму, и ?utm_source=anything сворачивается в одну запись кэша.

Стройте цепочки. Один Worker может иметь внешнюю точку входа для аутентификации и маршрутизации, точку входа нормализации для удаления параметров отслеживания и восстановления заголовков кодировки, кэшируемую точку входа, стоящую перед Durable Object, и отдельную кэшируемую точку входа для неаутентифицированного публичного API — каждая соединена этапом кэширования, который вы не настраивали, а просто решили, где его разместить. На странице примеров в документации разбираются несколько таких сценариев от начала до конца.

Мы не знаем другой платформы, где это возможно. CDN-кэши располагаются перед источником. Платформы для функций выполняют функции. Мы не знаем другой платформы, которая предоставляет кэш, находящийся внутри одного развёртываемого блока, между частями вашего приложения, причём каждый этап кэширования настраивается кодом по обе стороны от него. Именно таков Workers Cache. И поскольку он комбинируется со всем остальным, что платформа уже даёт — Smart Placement, Durable Objects, сервисные привязки, ctx.props, ctx.exports — паттерны, которые вы можете построить, открыты. В этой статье мы лишь едва коснулись поверхности.

Первая поддержка в вашем фреймворке

Если вы используете Astro, адаптер Cloudflare автоматически настраивает Workers Cache. Просто добавьте провайдер cacheCloudflare в вашу конфигурацию:

// astro.config.mjs
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

export default defineConfig({
  adapter: cloudflare(),
  output: "server",
  experimental: {
    cache: { provider: cacheCloudflare() },
    routeRules: {
      "/products/*": { maxAge: 300, swr: 3600, tags: ["products"] },
      "/blog/*":     { maxAge: 60,  swr: 86400, tags: ["blog"] },
    },
  },
});

Адаптер включает кэш, устанавливает правильные заголовки в ответах, генерируемых Astro, добавляет значения Cache-Tag для инвалидации и предоставляет вспомогательную функцию cache.invalidate() для сброса тегов при изменении контента. Страницы Astro, которые используют серверный рендеринг, автоматически получают описанный выше поток «отрисовать один раз, кэшировать, обновлять в фоне» — без дополнительной настройки на каждый маршрут, без изучения специфического для фреймворка слоя выполнения.

Мы работаем с мейнтейнерами других фреймворков, чтобы внедрить такую же интеграцию. Если вы создаёте адаптер фреймворка для Cloudflare, API Workers Cache именно такие, какими вы хотели бы их видеть — конфигурация через заголовки, программная очистка, никаких платформенно-специфичных концепций для моделирования.

Просматривайте свой кэш на той же панели, что и ваш Worker

Кэширование полезно только если вы видите, что оно делает. Панель наблюдения Workers Observability теперь отображает информацию о попаданиях в кэш для каждого вызова:

Ускорьте работу вашего Worker: собственный кэш на входе

Вы можете видеть для каждого Worker:

  • Коэффициент попаданий в кэш со временем. Число, которое после включения кэширования должно стремиться вверх.

  • Попадания (Hits), промахи (Misses), обновления (Updates), обходы (Bypasses) в разбивке. Если ваш коэффициент попаданий низкий, здесь вы узнаете причину — слишком много ответов BYPASS (потому что что-то устанавливает cookie?), слишком много ответов MISS (потому что ключ кэша разбивает данные сильнее, чем вы думали?), слишком много ответов UPDATING (потому что max-age меньше, чем интервал трафика?).

Поскольку всё это находится на той же панели, что и остальная наблюдаемость вашего Worker — логи, исключения, время CPU, количество запросов — вам не нужно переключаться между зоной и Worker, чтобы понять, что происходит.

Биллинг

Попадания в кэш не выполняют ваш Worker и не тарифицируют время CPU. Они учитываются как запрос по стандартному тарифу на запросы Workers, так же как любой другой вызов. Промахи и обходы кэша тарифицируются обычным образом — запрос + время CPU, точно так же, как без кэширования.

Результат

Плата за запрос

Плата за процессорное время

Попадание в кэш (HIT) — Worker не выполняется

Стандартный тариф

Не тарифицируется

Промах кэша (MISS) — Worker выполняется

Стандартный тариф

Тарифицируется

Обход кэша (BYPASS) — Worker выполняется

Стандартный тариф

Тарифицируется

Запрос статического ассета

Стандартный тариф

Не тарифицируется

Вызов Worker-к-Worker

Стандартный тариф

Тарифицируется, если Worker выполняется

Не существует отдельного артикула Workers Cache и отдельной платы за гигабайт кэш-хранилища. Многоуровневое кэширование, очистка, stale-while-revalidate и описанная выше аналитика включены. Если запрос должен был запустить ваш Worker, но Workers Cache обслуживает его как попадание, вы по-прежнему платите стандартную ставку за запрос, но не платите за процессорное время для этого запроса. Благодаря этому такое попадание в кэш обходится дешевле, чем генерация того же ответа в вашем Worker.

Обратите внимание: когда кэширование включено, запросы, которые обычно бесплатны — запросы статических ресурсов и вызовы от worker к worker через привязки сервисов или ctx.exports — тарифицируются по стандартной ставке за запрос, потому что каждый из них теперь обращается к кэшу перед вашим Worker.

Что дальше

Что мы планируем сделать в ближайшее время:

  • Более умное совместное размещение с Smart Placement. Сегодня Cloudflare выбирает верхний уровень кэша и цель Smart Placement независимо. При полном промахе запрос может пройти между локациями Cloudflare дважды: один раз для проверки верхнего уровня, и еще раз для запуска вашего Worker рядом с его данными. Мы работаем над координацией этих выборов, чтобы при промахе такой длинный путь совершался только один раз.

  • Увеличение лимитов на размер ответа. На момент запуска все ответы следуют лимиту размера кэшируемых данных для бесплатного плана (512 МБ), независимо от вашего аккаунта. Это временно — стандартные лимиты для каждого плана вступят в силу после завершения нескольких этапов развертывания.

  • Больше интеграций с фреймворками. Astro имеет встроенную интеграцию с Workers Cache. Мы работаем с разработчиками, чтобы добавить подобные интеграции в другие фреймворки, включая TanStack Start и Next.js через Vinext.

  • API для пометки кэшированных ответов как устаревших. ctx.cache.purge() удаляет соответствующие ответы из кэша. Мы рассматриваем API ctx.cache.invalidate(), который делает соответствующие ответы устаревшими, чтобы следующий запрос мог получить быстрый устаревший ответ с помощью stale-while-revalidate, пока ваш Worker обновляет кэш в фоне.

Попробуйте

Workers Cache доступен сегодня для каждого Worker на любом тарифном плане.

Чтобы начать, добавьте "cache": { "enabled": true } в ваш wrangler.jsonc, повторно разверните и начните устанавливать заголовки Cache-Control. Документация Workers Cache описывает все возможности — включая быстрый старт, ключи кэша, очистку, шаблоны композиции и примеры, а также отладку.

Ранее Workers работали перед кэшем. Теперь они могут работать и за ним. Используйте ту сторону, которая вам нужна, — или, с привязками сервисов, обе сразу.