Почему связка cache_key + Vary напрямую влияет на hit ratio
Когда вы включаете proxy_cache, ожидаете стабильный HIT на повторных запросах и заметное снижение нагрузки на апстрим. На практике hit ratio часто «плавает» или остаётся низким, даже если кеш большой и TTL выглядит разумным. Обычно причины две:
- слишком «раздутый» ключ кеша —
proxy_cache_keyменяется от запроса к запросу из-за лишних параметров, cookie и заголовков; - неучтённая вариативность ответа — приложение реально меняет тело ответа, а ключ этого не отражает (или, наоборот, дробит кеш слишком мелко).
В HTTP за вариативность отвечает заголовок Vary: он сообщает кешам, что ответ зависит от определённых заголовков запроса. Важный нюанс: в Nginx как reverse proxy заголовок Vary сам по себе не «подмешивается» в ключ автоматически. Nginx выбирает объект по proxy_cache_key, который вы задали (или оставили дефолтный). Если приложение отдаёт разные версии по Accept-Encoding или Accept-Language, а в ключе этого нет, вы рискуете получать неправильные ответы из кеша.
Продакшен-правило: всё, что реально меняет тело ответа, должно быть либо частью
proxy_cache_key, либо должно отключать кеширование (bypass и запрет записи).
Как Nginx выбирает объект в кеше: что такое proxy_cache_key
Ключ задаётся директивой proxy_cache_key. В продакшене лучше задавать ключ явно, чтобы поведение было предсказуемым при обновлениях конфигов и переносе между окружениями.
Базовый безопасный ключ для reverse proxy обычно включает:
- схему (
$scheme), чтобы не смешивать нетипичные схемы проксирования; - хост (
$host), если у вас несколько доменов на одном кеш-зоне; - URI с query string (
$request_uri), если параметры действительно влияют на ответ.
proxy_cache_key "$scheme://$host$request_uri";
Самая частая ошибка, убивающая hit ratio: оставлять в ключе «шумные» параметры (маркетинговые метки, трекинг, случайные идентификаторы), из-за которых каждый запрос уникален.
Быстрый тест: почему кеш «не попадает»
Если вы добавили отладочный заголовок (например, X-Cache-Status) или логируете $upstream_cache_status, типовые симптомы такие:
- MISS почти всегда — ключ слишком вариативный, маленький
inactive, либо кеш постоянно обходится условиямиproxy_cache_bypass/proxy_no_cache; - BYPASS — сработало правило обхода (cookie, authorization, метод, заголовок, аргумент);
- HIT есть, но «не тот контент» — забыли учесть Vary-факторы в ключе и склеили разные варианты ответа в один.

Vary header: что с ним делать в Nginx proxy_cache
Vary чаще всего встречается в сценариях:
Vary: Accept-Encoding— разные версии (gzip/br/identity);Vary: Accept-Language— локализация;Vary: Origin— CORS и мульти-ориджин API.
Если апстрим возвращает Vary, это сигнал: без учёта этих заголовков в ключе кеш может отдать неправильный ответ. В Nginx подход практический: вы сами добавляете в proxy_cache_key те факторы, которые действительно разделяют тело ответа.
Пример: учитывать Accept-Encoding
Если сжатие происходит на апстриме и он реально возвращает разные тела (например, br vs gzip), добавьте $http_accept_encoding в ключ. Но учтите: заголовок может быть очень вариативным (порядок значений, лишние токены), и это дробит кеш.
proxy_cache_key "$scheme://$host$request_uri|ae=$http_accept_encoding";
Компромисс для hit ratio: если вы сами сжимаете на Nginx, а к апстриму ходите за несжатым (или принудительно «снимаете» Accept-Encoding к апстриму), тогда дробление по Accept-Encoding часто можно убрать. Перед изменениями проверьте, где именно происходит компрессия и какие заголовки возвращает апстрим.
Пример: учитывать язык (лучше нормализованный)
Если локаль реально меняет HTML/JSON, добавляйте язык в ключ. Но Accept-Language тоже «шумный», поэтому лучше свести его к 2–3 значениям через map и класть в ключ нормализованный маркер.
map $http_accept_language $lang_key {
default en;
~*^ru ru;
~*^en en;
}
proxy_cache_key "$scheme://$host$request_uri|lang=$lang_key";
Так вы получаете корректность (разные языки не смешиваются) и высокий hit ratio (не плодите десятки вариантов ключей).
Authorization и private cache: где чаще всего делают утечки
Заголовок Authorization почти всегда означает персонализированный ответ. Если такой ответ попадёт в «общий» кеш, это риск утечки данных между пользователями. Поэтому базовая стратегия:
- если есть
Authorization— по умолчанию обход (самое безопасное); - если вам осознанно нужен private cache — ключ должен разделять пользователей без хранения секретов в явном виде.
Вариант 1 (рекомендуется для большинства): bypass по Authorization
map $http_authorization $skip_cache_auth {
default 1;
"" 0;
}
proxy_cache_bypass $skip_cache_auth;
proxy_no_cache $skip_cache_auth;
Да, это может снизить hit ratio в авторизованной зоне, зато резко снижает шанс закешировать приватный ответ.
Вариант 2: private cache для авторизованных (только если уверены)
Иногда нужно кешировать ответы «на пользователя» (например, тяжёлая агрегация, но одинаковая в рамках учётки). Тогда:
- делите кеш по идентификатору пользователя, а не по сырому токену;
- ставьте небольшой TTL;
- не кешируйте ответы, содержащие чувствительные данные, если нет жёсткой необходимости.
Рабочий паттерн: апстрим после проверки токена добавляет доверенный заголовок X-User-Id, и вы используете его для ключа.
proxy_cache_key "$scheme://$host$request_uri|uid=$upstream_http_x_user_id";
Важно: не используйте клиентский $http_x_user_id для private cache. Только то, что выставляет апстрим после валидации, и только если вы уверены, что этот заголовок нельзя подменить на пути.
Cookie: главный убийца hit ratio (и как его приручить)
Многие приложения выставляют cookie «на каждый чих»: сессии, A/B тесты, аналитика, флаги интерфейса. Если вы добавите $http_cookie в proxy_cache_key, hit ratio легко падает почти до нуля: cookie уникальны у каждого пользователя.
Практический подход для публичного контента:
- обход кеша при наличии «значимых» cookie (сессия, корзина, логин);
- маркетинговые/аналитические cookie игнорировать для кеша (не добавлять в ключ и не делать bypass);
- в спорных местах безопаснее отключить кеширование, чем пытаться «угадать» персонализацию.
Пример: bypass по cookie сессии
map $http_cookie $skip_cache_cookie {
default 0;
~*"(session|sessid|phpsessid|wordpress_logged_in)" 1;
}
proxy_cache_bypass $skip_cache_cookie;
proxy_no_cache $skip_cache_cookie;
Эта схема хорошо поднимает hit ratio на публичных страницах и снижает риск закешировать персональные ответы.
Cache busting: когда «обновить кеш» превращается в постоянные MISS
cache busting — это любая техника принудительного обхода кеша: уникальный query string, специальные заголовки, ручной purge (если реализован), смена ключа. В разработке удобно, в проде опасно: если фронтенд начинает добавлять уникальный параметр к каждому запросу (например, ?t=timestamp), вы фактически отключаете кеш и постоянно бьёте апстрим.
Практика, которая обычно работает:
- для статических ассетов используйте версионирование в имени файла, а не timestamp в query string;
- для API опирайтесь на TTL, ETag/If-None-Match и версионирование схемы;
- если нужен ручной обход — делайте отдельный служебный заголовок/параметр и ограничивайте доступ (IP/ACL/доступ к админке).
Управляемый bypass через заголовок
map $http_x_cache_bust $skip_cache_bust {
default 0;
1 1;
}
proxy_cache_bypass $skip_cache_bust;
proxy_no_cache $skip_cache_bust;
Так вы не ломаете hit ratio случайными параметрами и не приучаете фронтенд «всегда добавлять timestamp».
proxy_cache_bypass vs proxy_no_cache: в чём разница и где ошибаются
Эти директивы часто ставят рядом, но смысл разный:
proxy_cache_bypass— не брать ответ из кеша (запрос пойдёт на апстрим), но ответ всё ещё может быть сохранён;proxy_no_cache— не сохранять ответ в кеш.
Типовая ошибка: включить только bypass, например для авторизованных запросов, и в итоге начать записывать персональные ответы в кеш (просто не отдавая их тем же запросам из кеша). Для Authorization и «значимых» cookie почти всегда ставьте обе директивы.
Дизайн cache_key: практические рецепты для роста hit ratio
Цель простая: ключ должен быть достаточно «узким», чтобы не смешивать разные ответы, и достаточно «широким», чтобы не дробить кеш по незначимым деталям.
1) Нормализуйте query string: уберите мусор
Если сайт получает много параметров вроде utm_*, gclid, fbclid, и они не меняют контент, исключите их из ключа. В Nginx проще всего собрать ключ из значимых аргументов вместо $request_uri.
proxy_cache_key "$scheme://$host$uri?page=$arg_page&sort=$arg_sort";
Минус: нужно договориться с разработкой, какие параметры влияют на ответ. Плюс: hit ratio часто растёт кратно на каталогах, поиске и листингах.
2) Делите по классу устройства, а не по User-Agent
Если приложение действительно отдаёт разные версии HTML для mobile/desktop, не включайте в ключ $http_user_agent целиком: ключ станет почти уникальным. Вместо этого выделите 2–3 класса через map и добавляйте только класс.
3) Осторожнее с Origin и CORS
Если API зависит от Origin, у вас три варианта:
- не кешировать такие ответы;
- включать
$http_originв ключ, понимая, что hit ratio просядет при множестве origin; - нормализовать origin до ограниченного списка (например, только ваши фронтенды).
Если видите Vary: Origin, воспринимайте это как красный флаг для «общего» кеширования.

Как измерять hit ratio правильно: логи и метрики, а не «ощущения»
Hit ratio — это не только «сколько HIT». Важно понимать доли MISS, BYPASS, EXPIRED, STALE (если используете) и причины.
Минимум, который быстро окупается: добавить $upstream_cache_status в формат логов и агрегировать по location/URI. Пример:
log_format main_ext '$remote_addr - $host "$request" $status '
'cache=$upstream_cache_status '
'rt=$request_time urt=$upstream_response_time '
'ua="$http_user_agent"';
Если у вас кешируются большие ответы (HTML, JSON, файлы), отдельно проверьте отдачу по диапазонам и влияние Range: это легко ломает кеш и может снижать hit ratio на загрузках. По теме полезно: как Range влияет на кеширование в Nginx/Apache.
Типовые безопасные шаблоны конфигурации (соберите под себя)
Ниже «скелет» как отправная точка: явный ключ, обход по авторизации/сессии, управляемый bypass. Дальше вы точечно добавляете вариативность (язык, кодировка, origin) только там, где это реально меняет тело ответа.
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=appcache:200m max_size=10g inactive=60m use_temp_path=off;
map $http_authorization $skip_cache_auth {
default 1;
"" 0;
}
map $http_cookie $skip_cache_cookie {
default 0;
~*"(session|sessid|phpsessid|wordpress_logged_in)" 1;
}
map $http_x_cache_bust $skip_cache_bust {
default 0;
1 1;
}
server {
location / {
proxy_cache appcache;
proxy_cache_key "$scheme://$host$request_uri";
proxy_cache_bypass $skip_cache_auth $skip_cache_cookie $skip_cache_bust;
proxy_no_cache $skip_cache_auth $skip_cache_cookie $skip_cache_bust;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://upstream_app;
}
}
Если вы отдаёте контент с современными форматами изображений и делите ответы по заголовкам клиента, заранее продумайте нормализацию и ключ. См. также: как учитывать WebP/AVIF через map и не убить hit ratio.
Чеклист: повышаем hit ratio и не ломаем приватность
- Зафиксируйте
proxy_cache_keyявно, чтобы не зависеть от дефолтов. - Уберите «шум» из ключа: лишние query-параметры, сырые User-Agent, полные cookie.
- Проверьте
Varyу апстрима: всё, что меняет тело ответа, отражайте в ключе или отключайте кеширование. Authorizationпо умолчанию — bypass + no_cache. Private cache делайте только осознанно и безопасно.Cookie— отделяйте «значимые» от «мусорных»; по сессии почти всегда нужен обход.- Измеряйте: логируйте
$upstream_cache_status, иначе оптимизация превращается в гадание. - Контролируйте cache busting: никаких timestamp-аргументов «везде», только управляемые механизмы.
Частые грабли
Кешируете редиректы/ошибки и потом «не понимаете», почему всё сломалось
Если кешируете 301/302, убедитесь, что редирект стабилен и не зависит от cookie/языка/гео. Иначе один вариант может «залипнуть» и портить трафик. То же касается 404/500: кешировать можно, но с коротким TTL и пониманием причин.
Ключ включает $host, а контент одинаковый на нескольких доменах
Так hit ratio будет ниже: каждый домен получает свой кеш. Если контент полностью одинаковый и вы хотите общий кеш, используйте канонический хост в ключе (осознанно) и проверьте влияние на абсолютные ссылки, редиректы и SEO-логики приложения.
Vary говорит одно, а приложение реально зависит от другого
Иногда приложение меняет контент по cookie, но не выставляет адекватный Vary (а Vary: Cookie часто превращает кеш в тыкву). В таком случае ориентируйтесь на факт: если cookie влияет на ответ, кешируйте только при отсутствии этой cookie или делайте bypass.
Итог
Высокий hit ratio в Nginx — это не магия размера кеша. Это дисциплина: стабильный proxy_cache_key, осмысленная работа с Vary, строгие правила для Authorization и Cookie, плюс контроль над cache busting. Сделаете ключ слишком общим — получите риск неправильного или приватного контента. Сделаете слишком «мелким» — получите вечные MISS. Правильная середина обычно достигается нормализацией и явным обходом кеша для персональных сценариев.


