Кэш на уровне обратного прокси — дешёвый способ поднять performance без переписывания приложения. Но результат даёт не сам факт включённого cache, а правильный дизайн ключа proxy_cache_key. Один неверный заголовок или неучтённая cookie — и вы получите взрывное разрастание кэша, промахи вместо хитов или, что опаснее, персонализированные данные, разданные посторонним.
Что такое proxy_cache_key и как nginx собирает ключ
В nginx ключ кэширования для блока proxy_* задаётся директивой proxy_cache_key. По умолчанию используется:
proxy_cache_key $scheme$proxy_host$request_uri;
Это значит, что один и тот же путь с разными параметрами запроса — разные объекты. Домены и схема (HTTP/HTTPS) тоже разделяются. Этого часто недостаточно: ответы могут отличаться из-за Accept-Encoding, языка, устройства, авторизации, наличия специфичных cookies.
Важно понимать, как влияет Vary. Если ответ апстрима приходит с заголовком Vary, nginx хранит отдельные варианты для значений соответствующих заголовков запроса. Это позволяет корректно разделять, например, gzip и br варианты при Vary: Accept-Encoding. Отключать уважение к Vary через proxy_ignore_headers Vary почти всегда плохая идея: вы рискуете смешать несовместимые варианты в одном ключе.
Стратегия проектирования: разделяем API и статику
У API и статических ресурсов разные профили вариативности. Для API часто есть авторизация, локализация, CORS, иногда A/B-флаги; для статики — кодек сжатия (Accept-Encoding), возможные cookies браузеров (которые нужно игнорировать), контроль версий через query-строки.
Рабочая стратегия:
- Явно нормализуем заголовки запроса в небольшое число классов через
map. - Избегаем «сырого» включения длинных заголовков в
proxy_cache_key. - Чётко разделяем персонализированные запросы и публичный кэш: либо байпасим, либо в ключ добавляем ровно те факторы, что влияют на ответ.
- Для статики игнорируем «шумные» cookies и ненужные заголовки. Для API внимательно относимся к
Authorizationи сессионным cookies.
Accept-Encoding: нормализация через map
Accept-Encoding бывает длинным и вольным (порядок, q-факторы). Сырая подстановка в ключ приводит к множеству дубликатов. Правильнее свести его к классам: br, gzip, identity. Это стабилизирует ключ и увеличивает hit ratio.
# Нормализуем кодек сжатия
map $http_accept_encoding $ae {
~br br;
~gzip gzip;
default identity;
}
# Учитываем схему, хост, URI и нормализованный кодек
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae;
Если вы используете gzip_vary on или модуль brotli, апстрим или сам nginx обычно добавляет Vary: Accept-Encoding. Это гарантирует разделение вариантов. Но нормализовать всё равно стоит, иначе значение заголовка клиента станет частью ключа (через Vary) в сырых, «шумных» формулировках. Отдельно см. разбор WebP/AVIF через Nginx и map для кэша.

Cookies: когда включать в ключ, а когда байпасить
Cookies — главный источник как персонализации, так и случайной фрагментации кэша. Общие правила:
- Если ответ персонализирован (зависит от сессии/логина), публичный
cacheследует обходить:proxy_no_cacheиproxy_cache_bypassпо признаку авторизации. - Если ответ одинаковый для всех, но апстрим по привычке ставит
Set-Cookie(например, трекинг), можно игнорировать этот заголовок при кэшировании и отдаче. - Для статики большинство cookies не должны влиять на ключ вообще.
# Выделим «опасные» cookies, влияющие на персонализацию
map $http_cookie $has_session_cookie {
~(session|auth|logged_in)= 1;
default 0;
}
# Если есть авторизация — не кэшируем и не берём из кэша
proxy_no_cache $has_session_cookie;
proxy_cache_bypass $has_session_cookie;
# Игнорируем Set-Cookie от апстрима, если кэш публичный и контент не персонализирован
proxy_ignore_headers Set-Cookie;
Обратите внимание: игнорировать Set-Cookie можно только там, где вы уверены, что ответ не зависит от пользователя. Иначе получите утечку данных между посетителями.
Vary: Cookie и почему это опасно
Иногда апстрим ставит Vary: Cookie. Тогда любой отличающийся набор cookies становится частью ключа (через механизм Vary), что раздувает кэш до бесконечности. Лучше попросить приложение уточнить Vary (например, по конкретной cookie), либо на прокси нормализовать значения и устранить лишние заголовки.
Язык, устройство, CORS: нормализуем заголовки
Частые причины вариативности — язык (Accept-Language), класс устройства (User-Agent), CORS (Origin). С ними тоже работаем через map, сокращая количество вариантов:
# Язык (пример: RU/EN/прочее)
map $http_accept_language $lang {
~^ru ru;
~^en en;
default other;
}
# Класс устройства для медиазапросов или шаблонов
map $http_user_agent $device {
~Mobile mobile;
default desktop;
}
# CORS: если API реально отвечает по-разному для разных Origin
map $http_origin $origin_key {
"" none;
default $http_origin;
}
# Итоговый ключ (пример)
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae|lang:$lang|dev:$device|orig:$origin_key;
Не включайте в ключ сырое значение $http_user_agent — это гарантированная лавина вариантов. Нормализация — обязательна.
API: Authorization и публичные GET
Для API действуют дополнительные правила. По умолчанию nginx кэширует только GET/HEAD. Частные эндпоинты с Authorization или сессионными cookies не должны попадать в публичный кэш.
# Признак авторизованного запроса
map $http_authorization $has_auth_header {
~^(?i)Bearer 1;
~^(?i)Basic 1;
default 0;
}
# Объединяем с cookies
map "$has_auth_header$has_session_cookie" $api_private {
~1 1; # есть токен или сессионная cookie
default 0;
}
# Байпас и запрет кэша для приватных запросов
proxy_no_cache $api_private;
proxy_cache_bypass $api_private;
# Для публичных GET можно учесть CORS или язык
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae|lang:$lang|orig:$origin_key;
# Прочее (параметры времени жизни, фоновые апдейты)
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_background_update on;
proxy_cache_lock on;
Если у вас существуют публичные API-методы без авторизации, кэшируйте их смело, но следите за Vary и не тяните в ключ лишние заголовки. Для приватных методов лучше вовсе не включать cache или держать отдельный, сегрегированный слой (например, пользовательский кэш по токену в приложении). См. также кэширование в Redis/Memcached на уровне приложения.
Практически всё из описанного удобнее и безопаснее внедрять на отдельном окружении. Если вы поднимаете обратный прокси под свои задачи, используйте VDS с достаточным объёмом RAM/SSD под кэш и логирование.
Статика: игнорируем лишнее, учитываем версионирование
Статика обычно версионируется через хэши в путях или query-параметры (app.css?v=123). Такую статику удобно кэшировать практически навсегда на уровне прокси. Главные нюансы — кодек сжатия и отсутствие влияния cookies.
# Статика: общий ключ без cookies и прочих заголовков клиента
location ~* \\.(css|js|png|jpg|webp|svg|ico)$ {
proxy_cache STATIC;
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae;
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
expires 7d;
add_header Cache-Control public;
}
Если апстрим заранее отдает сжатую статику (.br или .gz), полезно хранить разные варианты по $ae, либо настроить gunzip on для унификации (учтите нагрузку на CPU). Для больших файлов учитывайте поддержку диапазонов и поведение прокси — см. заметку о работе Range и кэша в материале «HTTP Range в Nginx и Apache».
Стабилизация ключа: общие приёмы
- Нормализуем всё, что может бесконтрольно расти:
Accept-Encoding,Accept-Language,User-Agent,Origin. - Разграничиваем приватные запросы: байпас по
Authorizationи сессионным cookies. - Сохраняем в ключе только то, что реально влияет на ответ. Ничего лишнего.
- Не игнорируйте
Vary, если не понимаете последствий. Лучше привести заголовки к канонической форме. - Для статики держите ключ максимально коротким и стабильным, учтите лишь кодек сжатия и сам URI.
Диагностика: как понять, что ключ работает
Добавьте служебные заголовки и посмотрите на hit/miss:
add_header X-Cache $upstream_cache_status always;
add_header X-Cache-Key $proxy_cache_key always;
$upstream_cache_status покажет HIT/MISS/BYPASS/EXPIRED, а $proxy_cache_key даст канонический вид ключа (полезно при отладке map-логики). Проверьте, что персонализированные запросы маркируются как BYPASS, а публичные быстро выходят на HIT.

Типичные ошибки
- Сырой
$http_user_agentв ключе. Это мгновенно взрывает кэш и убивает производительность. - Игнорирование
Varyв надежде «сэкономить». Итог — смешение несовместимых вариантов и некорректная отдача. - Кэширование ответов с персональной
Set-Cookieбез байпаса по сессии. Риск утечек. - Учёт всех cookies в ключе «на всякий случай». Это эквивалентно отключению кэша.
- Отсутствие нормализации
Accept-Encoding. Процент хитов падает из-за разнообразия заголовков клиентов.
Готовые шаблоны
Универсальный ключ для публичного HTML
# Нормализации (как выше)
map $http_accept_encoding $ae { ~br br; ~gzip gzip; default identity; }
map $http_accept_language $lang { ~^ru ru; ~^en en; default other; }
map $http_user_agent $device { ~Mobile mobile; default desktop; }
# Ключ и базовая политика
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae|lang:$lang|dev:$device;
proxy_cache_valid 200 3m;
proxy_cache_background_update on;
proxy_cache_lock on;
add_header Vary Accept-Encoding,Accept-Language always;
Добавление Vary гарантирует корректное разделение на стороне клиентов и кэшей по пути (CDN, браузер), а нормализация удерживает число вариантов в разумных пределах.
API: публичные GET без авторизации
map $http_authorization $has_auth_header { ~^(?i)Bearer 1; ~^(?i)Basic 1; default 0; }
map $http_cookie $has_session_cookie { ~(session|auth|logged_in)= 1; default 0; }
map $http_origin $origin_key { "" none; default $http_origin; }
# Байпас приватного
map "$has_auth_header$has_session_cookie" $api_private { ~1 1; default 0; }
proxy_no_cache $api_private;
proxy_cache_bypass $api_private;
# Ключ для публичного API
proxy_cache_key $scheme$proxy_host$request_uri|orig:$origin_key|ae:$ae;
add_header Vary Origin,Accept-Encoding always;
proxy_cache_valid 200 5m;
proxy_cache_valid 404 30s;
Если CORS-ответы одинаковые для всех источников, лучше вовсе исключить Origin из ключа и не добавлять его в Vary.
Статика с версионированием
map $http_accept_encoding $ae { ~br br; ~gzip gzip; default identity; }
location /assets/ {
proxy_cache STATIC;
proxy_cache_key $scheme$proxy_host$request_uri|ae:$ae;
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
add_header Cache-Control public, max-age=604800;
}
Версионирование по имени файла или параметру запроса сведёт инвалидацию к обновлению ссылки; кэш можно держать долго без риска. Для защиты отдачи приватных ссылок и контроля TTL обратите внимание на техники с secure-link и кэш.
Обслуживание кэша и контроль роста
Даже идеальный ключ не спасёт от переполнения диска, если не настроены лимиты. Проверьте proxy_cache_path с max_size, сроками удаления и количеством уровней каталогов. Регулярно мониторьте процент хитов и число объектов: лавинообразный рост — сигнал, что где-то в ключ попало «сырьё» (например, сырой Origin от множества доменов).
Краткий чек-лист
- Определите классы вариативности: язык, кодек, устройство, CORS, авторизация.
- Соберите канонизаторы через
mapдля каждого класса. - Сегрегируйте API и статику: разные ключи и политики.
- Байпас по
Authorizationи сессионным cookies. - Не игнорируйте
Vary, но нормализуйте значения заголовков. - Для статики игнорируйте
Set-Cookie, держите длинный TTL и простой ключ. - Добавьте диагностические заголовки
X-CacheиX-Cache-Key.
Грамотный дизайн proxy_cache_key в nginx — это не добавление «всего» в ключ, а жёсткий отбор факторов, реальное нормализующее map и дисциплина в отношении приватных запросов. Тогда ваш cache станет предсказуемым, компактным и действительно ускорит сайт и API.


