Cache-Control, TTL и Vary: как правильно настроить кеширование на CDN
Автор: Кирилл Бодров, технический аккаунт-менеджер EdgeCenter.
Кеширование на CDN управляется HTTP-заголовками, которые origin-сервер отправляет вместе с каждым ответом. Главный из них — заголовок Cache-Control: его директивы определяют, кто может хранить копию ответа (браузер, CDN, прокси), сколько времени копия считается свежей и что делать, когда этот срок истёк. Заголовок Vary определяет, какие варианты одного и того же URL хранятся раздельно. Правила работы кеша описаны в стандарте RFC 9111 «HTTP Caching», опубликованном IETF в июне 2022 года.
Ниже — справочник по всем директивам, порядок вычисления TTL, разбор заголовка Vary и ключа кеша, готовые политики по типам контента и конфигурации для Nginx, Apache, 1С-Битрикс, WordPress и Next.js.
Коротко:
- TTL (время жизни копии в кеше) в HTTP задаётся директивой max-age заголовка Cache-Control, в секундах. Для CDN отдельный TTL задаёт директива s-maxage, она имеет приоритет над max-age в общем кеше.
- no-cache не запрещает кеширование. Директива no-cache разрешает хранить копию, но требует проверять её у источника (origin) перед каждой выдачей. Полный запрет хранения — директива no-store.
- Без заголовков кеш угадывает. Если в ответе нет ни Cache-Control, ни Expires, но есть Last-Modified, RFC 9111 допускает эвристический TTL — типично 10% от времени, прошедшего с последнего изменения файла.
- Заголовок Vary размножает объекты кеша. Vary: Accept-Encoding безопасен, Vary: User-Agent и Vary: Cookie фрагментируют кеш и обрушивают процент попадания в кеш (cache hit ratio).
- Версионированная статика кешируется на год: Cache-Control: public, max-age=31536000, immutable. HTML — с короткой свежестью или с обязательной проверкой.
- Директивы stale-while-revalidate и stale-if-error (RFC 5861) позволяют CDN отдавать устаревшую копию, пока идёт обновление или пока origin недоступен.
- Нормальный процент попадания в кеш для статики — 90–95% и выше. Значение ниже 80% почти всегда означает ошибку в заголовках или в ключе кеша.
Как кеш решает, можно ли отдать сохранённую копию
Кеш отдаёт сохранённую копию без обращения к источнику, только пока копия свежая (fresh). Когда срок свежести истёк, копия становится устаревшей (stale), и кеш обязан либо проверить её у источника, либо скачать заново. Вся настройка кеширования сводится к управлению двумя вещами: сроком свежести и способом проверки.
Свежесть и TTL
Срок свежести (freshness lifetime) — это время в секундах, в течение которого копия ответа считается актуальной. В практике CDN тот же срок называют TTL (Time To Live). Возраст копии (age) — время, прошедшее с момента, когда копия была получена от origin. Копия свежая, пока её возраст меньше срока свежести.
Общий кеш сообщает возраст копии в заголовке ответа Age. Значение Age: 3540 при max-age=3600 означает, что копия пролежала в кеше 59 минут и через минуту станет устаревшей.
Откуда кеш берёт TTL: порядок приоритета
Кеш вычисляет срок свежести по первому найденному источнику в таком порядке:
| Приоритет | Источник | Для какого кеша | Пример |
| 1 | Директива s-maxage | Только общий кеш (CDN, прокси) | s-maxage=86400 |
| 2 | Директива max-age | Весь | max-age=3600 |
| 3 | Заголовок Expires | Весь | Expires: Wed, 05 Nov 2026 10:00:00 GMT |
| 4 | Эвристика | Если явного TTL нет | ~10% от времени с момента Last-Modified |
Отдельно стоит заголовок CDN-Cache-Control, определённый в RFC 9213 «Targeted HTTP Cache Control» (июнь 2022). CDN, поддерживающий этот заголовок, использует его значение и игнорирует Cache-Control и Expires. Подробнее — в разделе про отдельный TTL для CDN.
Эвристический TTL — главный источник непредсказуемого поведения. Раздел 4.2.2 RFC 9111 разрешает кешу, не получившему явного срока, назначить срок самостоятельно — как долю времени, прошедшего с даты в заголовке Last-Modified, и упоминает 10% как типичное значение. Файл, не менявшийся год, без всяких заголовков может закешироваться больше чем на месяц. Поэтому каждый ответ источника должен нести явный Cache-Control, иначе решение принимает кеш.
Валидация: ETag, Last-Modified и ответ 304
Валидация (revalidation) — это проверка устаревшей копии у origin без повторного скачивания тела ответа. Кеш отправляет условный запрос (conditional request), а источник отвечает либо полным ответом, если ресурс изменился, либо коротким ответом 304 Not Modified, если не изменился.
Валидаторов два:
- ETag — заголовок ответа с идентификатором версии ресурса, например ETag: "5f3e-1a2b". Кеш передаёт этот идентификатор в заголовке запроса If-None-Match. Если версия совпала, источник возвращает 304.
- Last-Modified — заголовок ответа с датой последнего изменения ресурса. Кеш передаёт дату в заголовке запроса If-Modified-Since. Точность — одна секунда, поэтому для ресурсов, меняющихся чаще, надёжнее ETag.
Ответ 304 содержит только заголовки, без тела. Для страницы на 200 КБ валидация экономит почти весь трафик, но не экономит время на поездку до источника. Поэтому валидация дешевле повторной загрузки, но дороже выдачи свежей копии.
Приватный и общий кеш: кто хранит копию
На пути HTTP-ответа стоит кеш двух типов, и директивы Cache-Control адресуются по-разному.
Приватный кеш (private cache) — кеш, который обслуживает одного пользователя. Типичный пример — кеш браузера. Общий кеш (shared cache) — кеш, который хранит ответы для повторного использования многими пользователями. Типичные примеры — узел CDN, обратный прокси, корпоративный прокси. Оба определения даны в разделе 1 RFC 9111.
| Приватный кеш (браузер) | Общий кеш (CDN, прокси) | |
| Кого обслуживает | Одного пользователя | Всех пользователей узла |
| Можно ли хранить персональные данные | Да | Нет — один пользователь увидит данные другого |
| Директивы, адресованные только ему | private разрешает хранить только здесь | s-maxage, proxy-revalidate, public |
| Как сбросить | Только сменой URL или ожиданием TTL | Очистка кеша в панели или через API CDN |
| Чем управлять | Заголовком Cache-Control | Заголовками Cache-Control, CDN-Cache-Control и правилами CDN |
Вот почему кэш браузера невозможно сбросить извне. Если вы отдали файл с max-age=31536000 под постоянным именем, а потом изменили файл, пользователи увидят старую версию до истечения года или до очистки кеша вручную. Инвалидация (очистка кеша) на CDN кеш браузера не затрагивает. Отсюда правило: длинный TTL допустим только для файлов, у которых при каждом изменении меняется URL.
Справочник директив Cache-Control
Директивы заголовка Cache-Control перечисляются через запятую: Cache-Control: public, max-age=3600, stale-while-revalidate=60. Директивы ответа определены в разделе 5.2.2 RFC 9111, расширения — в RFC 5861 и RFC 8246.
| Директива |
Что делает |
Кому адресована |
Когда применять |
Источник |
| max-age=N |
Копия свежая N секунд |
Всему кешу |
Основной способ задать TTL |
RFC 9111, 5.2.2.1 |
| s-maxage=N |
Копия свежая N секунд, переопределяет max-age |
Только общему кешу |
Разный TTL для CDN и браузера |
RFC 9111, 5.2.2.10 |
| no-cache |
Хранить можно, перед выдачей проверить у origin |
Всему кешу |
HTML с ETag, данные, требующие актуальности |
RFC 9111, 5.2.2.4 |
| no-store |
Не сохранять ответ вообще |
Всему кешу |
Персональные данные, платёжные страницы |
RFC 9111, 5.2.2.5 |
| private |
Хранить может только приватный кеш |
Общему кешу — запрет |
Личный кабинет, корзина |
RFC 9111, 5.2.2.7 |
| public |
Хранить может любой кеш, даже при авторизации |
Всему кешу |
Статика; ответы на запросы с заголовком Authorization |
RFC 9111, 5.2.2.9 |
| must-revalidate |
Устаревшую копию нельзя отдавать без проверки, при недоступности источника — ошибка |
Всему кешу |
Где устаревшие данные недопустимы |
RFC 9111, 5.2.2.2 |
| proxy-revalidate |
То же, что must-revalidate, но только для общего кеша |
Общему кешу |
Где устаревшие данные недопустимы |
RFC 9111, 5.2.2.8 |
| no-transform |
Запрет изменять тело ответа |
Посредникам |
Защита от пересжатия изображений и видео |
RFC 9111, 5.2.2.6 |
| must-understand |
Хранить, только если кеш понимает требования кода ответа |
Всему кешу |
Редко, новые коды статуса |
RFC 9111, 5.2.2.3 |
| immutable |
Ресурс не изменится, пока копия свежая, не проверять при перезагрузке |
Всему кешу |
Версионированная статика |
RFC 8246 |
| stale-while-revalidate=N |
Отдавать устаревшую копию ещё N секунд, обновляя её в фоне |
Всему кешу |
HTML, API, каталоги |
RFC 5861 |
| stale-if-error=N |
Отдавать устаревшую копию N секунд, если источник отвечает ошибкой |
Всему кешу |
Защита от простоя сервера |
RFC 5861 |
Поддержка расширений из RFC 5861 различается между CDN-провайдерами и браузерами. Перед тем как строить на них стратегию отказоустойчивости, проверьте документацию своего провайдера и поведение на тестовом ресурсе.
no-cache и no-store: главная путаница
Директиву no-cache регулярно ставят, чтобы «отключить кеширование», и получают не тот результат.
Директива no-cache разрешает кешу сохранить ответ, но запрещает отдавать сохранённую копию без проверки у источника. На практике это означает условный запрос при каждом обращении. Если у ответа есть ETag, источник отвечает 304, и трафик почти не тратится. Для HTML-страниц это часто оптимальный режим: страница всегда актуальна, а передаётся только при изменении.
Директива no-store запрещает сохранять ответ в любом кеше, приватном или общем. Каждый запрос — полная загрузка. Применяется к ответам с персональными или платёжными данными, которые не должны оставаться на диске устройства.
Директива private запрещает хранение только общему кешу. Браузер сохранит ответ, CDN — нет. Подходит для личного кабинета, где повторная загрузка на том же устройстве допустима, а попадание данных в общий кэеш — нет.
|
Задача |
Правильная директива |
Типичная ошибка |
|
Страница всегда актуальна, но без лишнего трафика |
no-cache + ETag |
no-store — каждый раз полная загрузка |
|
Персональные данные не должны попасть на CDN |
private |
no-cache — CDN может сохранить ответ |
|
Данные не должны остаться на устройстве |
no-store |
private — браузер сохранит копию |
|
Отключить кеш на время отладки |
no-store |
max-age=0 без must-revalidate — копия может быть отдана устаревшей |
Отдельно о заголовке Pragma: no-cache. Это наследие HTTP/1.0, которое современный кеш учитывает только при отсутствии Cache-Control. В новых конфигурациях заголовок Pragma не нужен.
s-maxage и CDN-Cache-Control: отдельный TTL для CDN
Разный срок хранения для браузера и для CDN — самая полезная и самая недоиспользуемая возможность кеширования.
Директива s-maxage задаёт срок свежести только для общего кеша и переопределяет max-age на CDN и прокси. Браузер директиву s-maxage игнорирует. Пример для HTML каталога:
Cache-Control: public, max-age=60, s-maxage=600
Браузер хранит страницу минуту, CDN — десять минут. При обновлении каталога вы сбрасываете кеш CDN через purge, и через минуту все пользователи видят новую версию. Нагрузка на origin при этом определяется TTL на CDN, а не в браузере.
Заголовок CDN-Cache-Control (RFC 9213) идёт дальше: он адресован только кешу CDN и не затрагивает другой общий кеш, например корпоративные прокси. CDN, поддерживающий RFC 9213, использует значение CDN-Cache-Control и игнорирует Cache-Control и Expires. Пример из самого стандарта:
Cache-Control: max-age=60, s-maxage=120CDN-Cache-Control: max-age=600
Здесь кеш CDN хранит ответ 600 секунд, а другой общий кеш — 120 секунд, браузер — 60 секунд. Если CDN-Cache-Control отсутствует или содержит ошибку, CDN возвращается к обычному Cache-Control.
У ряда провайдеров есть собственные аналоги — например, заголовок Surrogate-Control. Поддержку RFC 9213 и собственных заголовков уточняйте в документации CDN.
Правила CDN поверх заголовков. Большинство CDN, включая EdgeCenter, позволяют задать TTL правилом в панели — по пути, расширению файла или условию. Важно понимать, какой источник главный: учитывает ли правило заголовки источника или переопределяет их. Рабочая схема: TTL задаётся заголовками на источнике, а правила CDN используются для исключений и для ресурсов, заголовки которых вы не контролируете.
stale-while-revalidate и stale-if-error
Две директивы из RFC 5861 «HTTP Cache-Control Extensions for Stale Content» (май 2010) превращают кеш из механизма ускорения в механизм отказоустойчивости.
Директива stale-while-revalidate=N разрешает кешу в течение N секунд после истечения свежести отдавать устаревшую копию немедленно и параллельно обновлять её на источнике в фоне. Пользователь не ждёт обновления кеша, следующий запрос получит свежую копию.
Cache-Control: max-age=300, stale-while-revalidate=60
Пять минут копия свежая. Ещё минуту — устаревшая, но отдаётся мгновенно, пока кеш обновляет её. После шести минут — обычная проверка на источнике.
Директива stale-if-error=N разрешает отдавать устаревшую копию в течение N секунд, если источник отвечает ошибкой 500, 502, 503 или 504. Сайт остаётся доступен, пока вы чините сервер.
Cache-Control: max-age=300, stale-if-error=86400
Для страниц каталога, статей, справочных разделов значение stale-if-error=86400 — сутки — недорогая страховка: даже полное падение сервера-источника не уронит сайт для пользователей, пока копии лежат на CDN.
immutable и версионированные файлы
Директива immutable (RFC 8246 «HTTP Immutable Responses», сентябрь 2017) сообщает браузеру, что ресурс не изменится, пока копия свежая, и проверять её при перезагрузке страницы не нужно.
Проблема, которую решает immutable: при обычной перезагрузке браузер отправляет условный запрос для каждого подресурса — каждого CSS, JS, шрифта, изображения. Большинство возвращают 304, но страница с сотней ресурсов тратит сотню поездок до сервера впустую. Директива immutable убирает эти запросы.
immutable безопасна только вместе с версионированием URL — приёмом, при котором любое изменение файла меняет его адрес:
- Хэш в имени файла: app.3f9a1c.js, styles.8b2e4d.css — самый надёжный вариант, его генерируют сборщики Webpack, Vite, esbuild
- Версия в пути: /assets/v42/app.js
- Параметр в URL: app.js?v=42 — работает, только если ключ кеша CDN учитывает этот параметр
Стандартная политика для версионированной статики:
Cache-Control: public, max-age=31536000, immutable
Год — фактический максимум, который имеет смысл задавать. При следующем изменении файл получит новое имя, а старая копия просто перестанет запрашиваться.
Vary: как один URL превращается в несколько объектов кеша
Заголовок Vary — это заголовок ответа, который перечисляет заголовки запроса, влияющие на содержимое ответа. Кеш хранит отдельную копию для каждого сочетания значений этих заголовков. Заголовок Vary определён в стандарте RFC 9110 «HTTP Semantics».
Пример: сервер отдаёт файл, сжатый Brotli, браузерам, которые его поддерживают, а остальным — сжатый gzip. Ответ содержит Vary: Accept-Encoding, и кеш хранит две копии — одну для каждого значения заголовка запроса Accept-Encoding. Без заголовка Vary кеш отдал бы браузеру без поддержки Brotli сжатую им копию, и браузер получил бы нечитаемый ответ.
|
Значение Vary |
Безопасность |
Почему |
|
Accept-Encoding |
Безопасно |
Мало вариантов, CDN обычно нормализует значения к br, gzip, без сжатия |
|
Accept |
С осторожностью |
Нужен для выдачи WebP и AVIF по одному URL, значения сильно различаются между браузерами — требуется нормализация на CDN |
|
Origin |
С осторожностью |
Нужен для CORS с разными разрешёнными доменами, варианты множатся по числу доменов |
|
Accept-Language |
Опасно |
Десятки вариантов строк, лучше разносить языки по разным URL |
|
User-Agent |
Опасно |
Тысячи уникальных строк — фактически отдельная копия на каждую версию браузера; процент попадания в кеш падает в разы |
|
Cookie |
Опасно |
Копия на каждого пользователя — общий кеш перестаёт работать |
|
* |
Запрет кеширования |
Ответ считается уникальным для каждого запроса |
Нормализация — способ сохранить пользу Vary без фрагментации. CDN приводит значение заголовка к небольшому набору вариантов до формирования ключа кеша: любой Accept-Encoding со словом br становится br, любой Accept с image/avif становится avif. Если ваш CDN поддерживает нормализацию, включайте её до того, как добавлять Vary на источник.
Если нужна разная вёрстка для мобильных устройств, правильное решение — адаптивная вёрстка с одним HTML для всех устройств. Если без разного HTML не обойтись — отдельный признак типа устройства, вычисляемый на CDN и включаемый в ключ кеша, а не Vary: User-Agent.
Ключ кеша: почему одинаковые страницы хранятся по сто раз
Ключ кеша (cache key) — это набор параметров запроса, по которому CDN ищет сохранённую копию. По умолчанию ключ обычно включает схему, домен, путь и строку query-параметров. Два запроса с разными ключами — это два разных объекта кеша, даже если ответ одинаковый.
Главный источник раздувания ключа — query-параметры, не влияющие на содержимое:
/catalog/shoes/?utm_source=yandex&utm_medium=cpc/catalog/shoes/?utm_source=vk&utm_campaign=autumn/catalog/shoes/?yclid=81234567&gclid=Cj0KCQ
Для CDN это три разных объекта, хотя страница одна. Каждый рекламный клик с уникальным идентификатором — промах кеша и запрос к origin.
Как настроить ключ кеша:
- Белый список параметров. В ключ включаются только параметры, меняющие содержимое: page, sort, color, size. Все остальные отбрасываются при формировании ключа, но передаются на источник, если нужны аналитике.
- Исключение рекламных меток. Если белый список невозможен, исключаются известные метки: utm_*, yclid, gclid, fbclid, _openstat, from.
- Сортировка параметров. ?a=1&b=2 и ?b=2&a=1 должны давать один ключ.
- Cookie в ключе — только по необходимости. Если сайт различает гостя и авторизованного пользователя, в ключ добавляется признак авторизации, а не весь заголовок Cookie.
Правильно настроенный ключ кеша — это ещё и защита. Атака cache-busting, при которой к каждому запросу добавляется уникальный параметр, при белом списке параметров перестаёт доходить до источника. Подробно — в материале о DDoS-атаках.
Готовые политики по типам контента
|
Тип контента |
Cache-Control на источнике |
Комментарий |
|
Версионированные JS, CSS, шрифты |
public, max-age=31536000, immutable |
Только при хэше или версии в URL |
|
Изображения без версии в URL |
public, max-age=86400, stale-while-revalidate=604800 |
Сутки свежести, неделя мягкого обновления |
|
HTML-страницы статей и каталога |
public, max-age=0, s-maxage=600, stale-while-revalidate=60, stale-if-error=86400 |
Браузер проверяет всегда, CDN хранит 10 минут, очистка кеша при публикации |
|
Главная страница под высокой нагрузкой |
public, max-age=0, s-maxage=5, stale-while-revalidate=30 |
Микрокеширование |
|
Личный кабинет, корзина, оформление заказа |
private, no-cache |
Не попадает на CDN, браузер проверяет каждый раз |
|
Платёжные страницы, страницы с персональными данными |
no-store |
Не сохраняются нигде |
|
Публичный API: справочники, курсы, остатки |
public, max-age=30, stale-while-revalidate=30 |
Значения подбираются под допустимую задержку данных |
|
Сегменты видео HLS и DASH (VOD) |
public, max-age=31536000, immutable |
Сегмент записи не меняется никогда |
|
Плейлист прямого эфира .m3u8 |
public, max-age=1 или no-cache |
Плейлист обновляется с каждым новым сегментом |
|
robots.txt, sitemap.xml |
public, max-age=3600 |
Час — компромисс между нагрузкой и актуальностью для роботов |
Эти значения — отправная точка. Итоговые TTL подбираются по двум вопросам: как быстро пользователи должны увидеть изменение и умеете ли вы сбрасывать кеш при публикации.
Конфигурации: Nginx, Apache, 1С-Битрикс, WordPress, Next.js
Nginx
|
# Версионированная статика # HTML: браузер проверяет, CDN хранит 10 минут # Личный кабинет |
Две особенности Nginx, на которых ошибаются чаще всего. Первая: директива add_header во вложенном блоке location полностью отменяет все add_header родительского уровня — если на уровне server добавлены заголовки безопасности, во вложенном location их придётся повторить. Вторая: без параметра always заголовок добавляется только к ответам с кодами 200, 201, 204, 206, 301, 302, 303, 304, 307 и 308.
Apache
|
<IfModule mod_headers.c> # ETag одинаковый на всех серверах кластера |
Директива FileETag MTime Size убирает inode из вычисления ETag. Без неё ETag одного файла различается на разных серверах за балансировщиком, и валидация не работает.
1С-Битрикс
1С-Битрикс подключает CSS и JS с параметром времени изменения: template_styles.css?1729845123. Это версионирование через query-параметр, поэтому ключ кеша CDN должен учитывать строку запроса для статики шаблонов. Если вы настроили CDN отбрасывать все параметры, пользователи будут получать старые стили после каждого обновления шаблона.
Что не кешируется на CDN ни при каких условиях: административная часть /bitrix/admin/, служебные обработчики /bitrix/tools/, /bitrix/services/, страницы личного кабинета, корзины и оформления заказа. Признак авторизованного пользователя — cookie с префиксом BITRIX_SM_ и сессионная cookie PHPSESSID: запросы с ними лучше не обслуживать из общего кеша HTML.
WordPress
WordPress не выставляет заголовки кеширования для статики — их задаёт веб-сервер, по образцу выше. Для HTML важно исключить из общего кэша:
- Запросы с cookie wordpress_logged_in_* и comment_author_*
- Для WooCommerce — cookie woocommerce_items_in_cart, woocommerce_cart_hash, wp_woocommerce_session_* и страницы корзины, оформления, личного кабинета
- Пути /wp-admin/, /wp-login.php, /wp-json/ для авторизованных запросов
При публикации или обновлении записи нужна инвалидация соответствующих URL на CDN — через плагин интеграции с CDN или вызов API провайдера из хука публикации.
Next.js
В production-сборке Next.js сам отдаёт статику из каталога /_next/static/ с заголовком Cache-Control: public, max-age=31536000, immutable — имена этих файлов уже содержат хэш. Для остальных путей заголовки задаются в next.config.js:
| module.exports = { async headers() { return [ { source: '/blog/:slug*', headers: [ { key: 'Cache-Control', value: 'public, max-age=0, s-maxage=600, stale-while-revalidate=60' }, ], }, ]; }, }; |
Инвалидация: как сбросить кеш на CDN
Инвалидация (purge) — принудительное удаление копий из кеша CDN до истечения TTL. Инвалидация не затрагивает кеш браузера: если браузер получил ответ с max-age=86400, purge на CDN не заставит его перезапросить ресурс раньше.
|
Способ инвалидации |
Как работает |
Когда применять |
|
По URL |
Удаляется копия конкретного адреса |
Обновили одну страницу или файл |
|
По префиксу или маске |
Удаляются все копии в каталоге |
Обновили раздел каталога |
|
По тегу |
Удаляются все копии, помеченные тегом в заголовке ответа |
Обновили товар, который выводится на сотне страниц |
|
Полная очистка |
Удаляется весь кеш ресурса |
Только в аварийной ситуации |
Полная очистка опасна. После неё вся нагрузка разом приходится на сервер-источник — эффект, похожий на атаку собственными силами. На сайте под нагрузкой полную очистку проводят в часы минимального трафика.
Инвалидация по тегу — самый удобный способ для каталогов и медиа: источник добавляет к ответу заголовок с идентификаторами сущностей на странице (товар, категория, автор), и при изменении товара сбрасываются все страницы с его тегом. Название заголовка и поддержка зависят от провайдера.
Надёжная схема для команды: статика версионируется и никогда не сбрасывается, HTML сбрасывается автоматически по URL или тегу из процесса публикации, полная очистка не используется.
Процент попадания в кеш: как считать и как поднять
Процент попадания в кеш (cache hit ratio) — доля запросов, обслуженных из кеша CDN без обращения к источнику. Это главный показатель качества настройки кеширования.
Различают две метрики:
- По запросам — доля запросов из кеша
- По байтам (byte hit ratio) — доля трафика из кеша
Метрики расходятся: на сайте с крупными видео и множеством мелких некешируемых API-запросов в кеше может быть 60%, а байтам — 97%. Для оценки нагрузки на источник важнее первая, для оценки исходящего трафика и счёта — вторая.
Как поднять процент попадания в кеш:
- Явный Cache-Control на каждом ответе источника — убирает эвристику.
- Белый список query-параметров в ключе кеша — убирает дубли от рекламных меток.
- Удаление Vary: User-Agent и Vary: Cookie — убирает фрагментацию.
- Версионирование статики и годовой TTL — статика перестаёт выпадать из кеша.
- s-maxage для HTML — CDN хранит страницы дольше браузера.
- Шилдинг источника — запросы с разных узлов CDN к источнику объединяются.
- Мониторинг: внезапное падение cache hit ratio при росте трафика — сигнал ошибки конфигурации или атаки cache-busting. Настройте на это алерт.
Как проверить заголовки кеширования
Через curl:
curl -sSI https://example.ru/static/app.3f9a1c.js | grep -iE "cache-control|age|etag|last-modified|vary|x-cache"
Выполните команду дважды. Первый ответ может быть промахом, второй должен показать попадание в кеш: заголовок статуса кеша от провайдера (часто X-Cache: HIT или аналог) и ненулевой заголовок Age.
Проверка валидации:
curl -sSI https://example.ru/ -H 'If-None-Match: "5f3e-1a2b"'
При совпадении ETag ожидается ответ 304 Not Modified.
Через DevTools браузера. На вкладке Network в колонке Size видно memory cache или disk cache, если ресурс взят из кеша браузера. В заголовках ответа — Cache-Control и Age.
Типичные ошибки
- Нет Cache-Control на ответах origin. Кеш назначает эвристический TTL, поведение непредсказуемо, процент попадания в кеш низкий.
- no-cache вместо no-store для персональных данных и наоборот — no-store там, где достаточно no-cache с ETag.
- Годовой TTL без версионирования URL. Пользователи месяцами видят старые файлы, и сбросить кеш браузера извне невозможно.
- Vary: User-Agent или Vary: Cookie. Кеш фрагментируется на тысячи копий.
- Рекламные метки в ключе кэша. Каждый клик по рекламе — запрос к источнику.
- Кеширование HTML авторизованных пользователей в общем кеше. Один пользователь видит имя, корзину или данные другого — критическая ошибка безопасности.
- ETag с inode на кластере серверов. Валидация никогда не возвращает 304.
- Полная очистка кеша в пиковое время. Сервер-источник получает весь трафик разом и падает.
- Повтор add_header в Nginx забыт. Вложенный location молча теряет заголовки безопасности и кеширования родительского уровня.
Часто задаваемые вопросы
Что такое Cache-Control? Cache-Control — это HTTP-заголовок, которым сервер сообщает кешу браузера, CDN и прокси, можно ли сохранять ответ, сколько времени копия считается свежей и что делать после истечения этого срока. Заголовок состоит из директив, например max-age, no-cache, private. Правила описаны в стандарте RFC 9111.
Чем no-cache отличается от no-store? Директива no-cache разрешает сохранить ответ, но требует проверять копию у сервера перед каждой выдачей — при неизменном ресурсе сервер отвечает коротким 304. Директива no-store запрещает сохранять ответ в любом кеше, и каждый запрос загружает ответ полностью.
Что такое TTL в кешировании? TTL (Time To Live) — время в секундах, в течение которого копия ответа в кеше считается свежей и отдаётся без обращения к серверу. В HTTP TTL задаётся директивой max-age заголовка Cache-Control, для CDN — директивой s-maxage или заголовком CDN-Cache-Control.
Чем max-age отличается от s-maxage? Директива max-age задаёт срок свежести для всего кеша, включая браузер. Директива s-maxage действует только в общем кеше — CDN и прокси — и переопределяет там max-age. Сочетание max-age=60, s-maxage=600 означает, что браузер хранит данные минуту, а CDN — десять минут.
Какой TTL ставить для статики? Для файлов с хэшем или версией в URL — год: public, max-age=31536000, immutable. Для изображений и файлов без версии в адресе — от часа до суток с stale-while-revalidate. Длинный TTL без версионирования URL опасен: кеш браузера нельзя сбросить извне.
Зачем нужен заголовок Vary? Заголовок Vary сообщает кешу, что ответ зависит от перечисленных заголовков запроса, и кеш должен хранить отдельную копию для каждого их сочетания. Пример безопасного использования — Vary: Accept-Encoding для раздельного хранения версий в Brotli и gzip. Vary: User-Agent и Vary: Cookie дробят кеш на тысячи копий.
Почему CDN не кеширует мои файлы? Типичные причины: источник отдаёт no-store, private или no-cache, в ответе есть заголовок Set-Cookie, из-за которого CDN не сохраняет копию, в ключ кеша попадают уникальные query-параметры, ответ содержит Vary: *, Vary: Cookie или Vary: User-Agent. Проверьте заголовки командой curl -I дважды подряд.
Что такое stale-while-revalidate? Директива stale-while-revalidate=N из RFC 5861 разрешает кешу в течение N секунд после истечения свежести отдавать устаревшую копию мгновенно, одновременно обновляя её у сервера в фоне. Пользователь не ждёт обновления, а следующий запрос получает свежую копию.
Сбрасывает ли очистка кеша на CDN кеш браузера? Нет. Инвалидация удаляет копии только на узлах CDN. Браузер, получивший ответ с длинным max-age, продолжит использовать свою копию до истечения срока. Поэтому для часто меняющихся страниц браузеру дают короткий TTL или no-cache, а длинный срок оставляют CDN через s-maxage.
Что такое процент попадания в кеш и какая норма? Это доля запросов, обслуженных из кеша CDN без обращения к источнику. Для статических файлов нормой считается 90–95% и выше. Значение ниже 80% обычно указывает на отсутствие заголовков Cache-Control, лишний Vary или рекламные метки в ключе кеша.