АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Почему nginx не кэширует ответ 200 даже с proxy_cache_valid — и чем заканчивается «просто игнорировать Set-Cookie»

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~24 мин чтения
Почему nginx не кэширует ответ 200 даже с proxy_cache_valid — и чем заканчивается «просто игнорировать Set-Cookie»
Иллюстрация к статье «Почему nginx не кэширует ответ 200 даже с proxy_cache_valid — и чем заканчивается «просто игнорировать Set-Cookie»».

Знакомая картина: в конфиг добавлен proxy_cache_path, прописан proxy_cache_valid 200 10m, а в логе по-прежнему сплошной MISS и бэкенд лежит под той же нагрузкой. Дальше обычно происходит одно и то же — человек находит в интернете строчку proxy_ignore_headers Cache-Control Set-Cookie, вставляет её, кэш «начинает работать», и через сутки пользователи видят чужие личные кабинеты. Ниже — почему nginx отказывается кэшировать, что именно ему мешает, как я разделяю кэшируемое и персональное на боевых стендах и как это проверить до того, как проверит клиент.

proxy_cache_valid — это не приказ, а значение по умолчанию

Первое, что надо принять: в nginx решение «кэшировать или нет» принимает не ваш конфиг, а ответ бэкенда. В русской документации ngx_http_proxy_module это написано прямым текстом в описании proxy_cache_valid: «Параметры кэширования могут также быть заданы непосредственно в заголовке ответа. Такой способ приоритетнее, чем задание времени кэширования с помощью директивы». Вы написали 10 минут — приложение прислало Cache-Control: no-store — победило приложение.

Из-за этого весь популярный способ отладки «а давайте увеличим срок кэша» бессмысленен по построению. Если ответ в принципе не признан кэшируемым, то что 10 минут, что 10 часов — разницы ноль, вы всё равно получите MISS на каждый запрос. Я видел конфиг, где человек последовательно поднял proxy_cache_valid с 5m до 12h, потом добавил proxy_cache_valid any 12h, потом ещё proxy_cache_min_uses 1 — и всё это на приложении, которое на каждый ответ ставит сессионную куку. Ни один из шагов не мог помочь.

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

Ещё один момент, который экономит часы: nginx принимает решение один раз, в момент получения ответа от апстрима. Он не «пересматривает» кэшируемость задним числом и не логирует причину отказа — в error_log вы ничего про это не увидите даже на уровне warn. Единственный внятный сигнал — значение $upstream_cache_status, и то оно скажет MISS, но не объяснит, почему. Поэтому диагностика всегда начинается с сырых заголовков апстрима, а не с чтения логов nginx.

Пока `curl` к бэкенду напрямую показывает `Set-Cookie` на статичной по смыслу странице — правьте приложение, а не nginx. Всё остальное будет борьбой со следствием.
Цифры и версии: proxy_cache_valid — это не приказ, а значение по умолчанию — схема
Цифры и версии: proxy_cache_valid — это не приказ, а значение по умолчанию. Открыть схему в полном размере

Полный список того, что останавливает nginx

Правила разбросаны по документации, поэтому соберу их в одном месте. Часть из них прямо перечислена на nginx.org/ru в описании proxy_cache_valid (Set-Cookie, Vary: *, X-Accel-Expires: 0), разбор директив Cache-Control описан в NGINX Admin Guide и блоге NGINX. nginx считает ответ некэшируемым, если: в ответе есть поле Set-Cookie; в Cache-Control встретились private, no-cache или no-store; есть Vary со специальным значением «*»; X-Accel-Expires равен нулю; истёкшее значение Expires в прошлом; метод запроса не входит в proxy_cache_methods (по умолчанию там только GET и HEAD, причём GET и HEAD кэшируются всегда); срок жизни не задан заголовками ответа, а код ответа не перечислен в proxy_cache_valid; сработало условие proxy_no_cache. Плюс есть отдельная тонкость: если запрос отправлен методом, который не кэшируется, nginx честно сходит на бэкенд.

Отдельно про Vary. С версии 1.7.7 nginx умеет учитывать Vary: если бэкенд прислал Vary: Accept-Encoding, ответ будет закэширован с учётом соответствующего заголовка запроса. Звучит хорошо, но на практике я регулярно вижу Vary: Cookie от фреймворков — и тогда каждый уникальный набор кук порождает свой вариант в кэше. Формально кэш работает, HIT есть, но hit ratio уезжает в единицы процентов, а keys_zone пухнет. Это не ошибка nginx, это ошибка бэкенда, который на всякий случай варьирует по кукам.

И про no-cache стоит сказать честно, потому что это место, где nginx расходится с RFC 9111. По стандарту no-cache означает «храни, но перед выдачей ревалидируй», а не «не храни». nginx в базовой обработке заголовков трактует no-cache как запрет на кэширование ответа. Формально это упрощение, практически — оно ловит больше проблем, чем создаёт, но знать о расхождении надо: приложение, которое ставит no-cache в надежде на условные запросы, у вас за nginx просто перестанет кэшироваться.

Про X-Accel-Expires добавлю отдельно, потому что это самый недооценённый заголовок в связке. Он задаёт время кэширования в секундах и имеет приоритет над Expires и Cache-Control, а значение с префиксом @ трактуется как абсолютное время в секундах от Epoch. Нулевое значение выключает кэширование конкретного ответа. Для приложения это идеальный способ управлять кэшем фронта, ничего не ломая в браузерных кэшах: X-Accel-Expires nginx съедает сам и наружу не отдаёт, потому что все заголовки X-Accel-* по умолчанию скрываются от клиента. Я почти всегда предлагаю разработчикам именно этот механизм вместо игр с Cache-Control, когда нужно поведение «для nginx одно, для браузера другое».

«proxy_cache_valid 5m;» без списка кодов кэширует только 200, 301 и 302. Если у вас API отдаёт 404 или 204 и вы ждёте их в кэше — их там не будет, и это не баг.
Почему nginx не кэширует ответ 200 даже с proxy_cache_valid — и чем заканчивается «просто игнорировать Set-Cookie» — схема
Схема к статье. Открыть схему в полном размере

Разбор: как одна строка конфига выдала чужие личные кабинеты

Стенд — груминг-салон «Пушистый стиль» на 16 рабочих мест: сайт с онлайн-записью и личным кабинетом клиента, где хранятся карточки питомцев, история стрижек, бонусный счёт и телефон владельца. Внутри — PHP-приложение на Laravel за nginx 1.28.3 из репозитория nginx.org, Debian 12, один VPS с php-fpm и MySQL. Раздел услуг и прайс с галереей работ открыт всем, а под кабинетом — персональные данные клиентов. Задача была банальная: после рассылки акции страницы прайса и витрины зоокосметики (около 1 800 позиций) генерились по 700–900 мс, и в первые часы после письма php-fpm вставал в очередь, а администраторы на ресепшене не могли открыть расписание мастеров.

Фрилансер, который делал сайт салона, включил кэш вот таким блоком — привожу как было, это ровно тот сниппет, который гуляет по статьям про nginx caching:

proxy_cache_path /var/cache/nginx/salon levels=1:2 keys_zone=salon:16m
                 max_size=2g inactive=60m use_temp_path=off;

location / {
    proxy_pass http://salon_upstream;
    proxy_cache salon;
    proxy_ignore_headers Cache-Control Set-Cookie;
    proxy_cache_valid any 30m;
}

Что произошло дальше. Laravel на каждый ответ ставит две куки — XSRF-TOKEN и сессионную laravel_session. Директива proxy_ignore_headers Set-Cookie отключила обработку этого поля, то есть nginx перестал видеть в нём причину не кэшировать. Но она не удаляет заголовок из ответа: Set-Cookie как лежал в теле кэшируемого ответа, так и лёг в файл кэша вместе с ним. Ключ кэша при этом остался дефолтным — $scheme$proxy_host$request_uri, никакого идентификатора пользователя в нём нет. Итог: первая клиентка, открывшая кабинет, положила в кэш свой ответ вместе со своей сессионной кукой, а следующие 30 минут все остальные получали эту куку из кэша и оказывались в её кабинете — с её телефоном, кличками её собак и записью на субботу. Симптом, с которым нас позвали, звучал как «клиенты звонят и говорят, что видят чужих питомцев».

Разбирали по логам: в access_log добавили $upstream_cache_status, за сутки 1 940 запросов на /cabinet/*, из них 1 810 HIT — то есть кабинет отдавался из кэша практически всегда. Дальше просто: вернули location для кабинета в режим без кэша, вычистили /var/cache/nginx/salon, перезапустили nginx и инвалидировали все сессии на стороне приложения. Готовой artisan-команды «сбросить все сессии» в Laravel нет, поэтому действовали по драйверу сессий: при драйвере file очистили storage/framework/sessions, при database — таблицу sessions; дополнительно поменяли имя сессионной куки (SESSION_COOKIE), чтобы старые куки из кэша стали бесполезны. Всем клиентам пришлось войти заново, но иначе утёкшие куки оставались бы валидными. Потом уже спокойно собрали нормальную схему кэширования, о ней ниже. Прайс и витрина после переделки отдавались за 8–14 мс с hit ratio около 90 %, php-fpm перестал упираться в очередь даже в день рассылки. Отдельно владелице салона пришлось решать вопрос уведомления клиентов: телефоны и имена — это персональные данные.

Отдельно проговорю, почему подрядчик вообще так сделал, — это не глупость, а типовая ловушка. Он честно проверил конфиг: открыл каталог в анонимном браузере, увидел X-Cache-Status: HIT, время ответа упало с 800 мс до 10 мс, задача закрыта. Кабинет он не проверял, потому что «кабинет же за авторизацией». А ловушка в том, что location / накрывает и кабинет тоже, и первый же авторизованный клиент, зашедший на /cabinet/, положил свой ответ в общую запись. Проверка «работает ли кэш» и проверка «не течёт ли кэш» — это два разных теста, и второй почти никогда не делают.

Если вы всё-таки обязаны игнорировать Set-Cookie (например, бэкенд ставит бесполезную аналитическую куку на статике), обязательно ставьте рядом `proxy_hide_header Set-Cookie;`. Учтите, что эта директива лишь не передаёт поле клиенту: в файле кэша заголовок останется, но наружу — ни с бэкенда, ни из кэша — уже не уйдёт. Игнорировать без скрытия — это не оптимизация, это инцидент.
Цифры и версии: Разбор: как одна строка конфига выдала чужие личные кабинеты — схема
Цифры и версии: Разбор: как одна строка конфига выдала чужие личные кабинеты. Открыть схему в полном размере

Как я это делаю на боевых стендах: разделяй, а не игнорируй

Моя позиция простая: proxy_ignore_headers — не инструмент включения кэша, а инструмент точечного обхода кривого бэкенда, и применять его надо в максимально узком location. Кэш включается не «на весь сайт», а на конкретные, заведомо анонимные маршруты. Всё остальное идёт на бэкенд без кэша, и это нормально — попытка закэшировать вообще всё почти всегда заканчивается либо нулевым hit ratio, либо утечкой.

Рабочая схема, к которой я прихожу практически на каждом проекте, выглядит так. Отдельная зона кэша, отдельный location под анонимный контент, явное перечисление кодов ответа, и обязательный аварийный выключатель по куке авторизации.

proxy_cache_path /var/cache/nginx/anon levels=1:2 keys_zone=anon:16m
                 max_size=2g inactive=24h use_temp_path=off;

map $http_cookie $is_logged_in {
    default            0;
    "~*laravel_session" 1;
    "~*PHPSESSID"       1;
    "~*BITRIX_SM_LOGIN" 1;
}

location ^~ /services/ {
    proxy_pass http://salon_upstream;

    proxy_cache        anon;
    proxy_cache_key    "$scheme$host$request_uri";
    proxy_cache_valid  200 301 302 10m;
    proxy_cache_valid  404 1m;
    proxy_cache_min_uses 2;

    proxy_cache_bypass $is_logged_in $http_authorization;
    proxy_no_cache     $is_logged_in $http_authorization;

    proxy_cache_lock         on;
    proxy_cache_lock_timeout 5s;
    proxy_cache_revalidate   on;
    proxy_cache_background_update on;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

    add_header X-Cache-Status $upstream_cache_status always;
}

Разберу неочевидное. proxy_cache_key я почти всегда переопределяю: дефолтный $scheme$proxy_host$request_uri содержит имя апстрима, а не хост запроса, и на мультидоменной конфигурации два разных сайта, ходящих в один upstream, схлопнутся в одну запись кэша. Это ещё один способ выдать чужой контент, только уже между доменами. proxy_cache_min_uses 2 отсекает мусорный длинный хвост URL, который спрашивают ровно один раз, — он экономит и место, и записи в keys_zone. Связка proxy_cache_use_stale updating + proxy_cache_background_update + proxy_cache_lock — то, что реально держит бэкенд живым в момент истечения популярной записи: без них истечение горячего URL превращается в залп одновременных запросов на апстрим.

Про размер зоны. keys_zone задаётся в мегабайтах, и, по документации, зоны размером в 1 мегабайт достаточно для хранения около 8 тысяч ключей (в коммерческой версии — около 4 тысяч, там в зоне хранится расширенная информация). На сайте салона уникальных URL прайса и витрины было около 4 тысяч с учётом фильтров и пагинации — формально хватило бы и мегабайта, взяли 16 МБ с большим запасом, память это не экономит заметно. Если keys_zone всё же кончится, nginx начнёт принудительно вытеснять наименее востребованные записи независимо от того, сколько места осталось на диске, а в error_log появятся сообщения уровня alert вида «could not allocate node in cache keys zone». Hit ratio при этом просядет, и если error_log никто не читает, это пройдёт незамеченным. Это, кстати, ещё одна причина не сваливать всё в одну зону: переполнение зоны статикой выбьет из кэша прайс.

`inactive` и `proxy_cache_valid` — не синонимы. `proxy_cache_valid 10m` означает «через 10 минут ответ протух и требует обновления», `inactive=24h` — «если к записи сутки никто не обращался, файл удаляется». Запись может быть протухшей, но живой — именно из неё отдаётся STALE.

proxy_no_cache и proxy_cache_bypass: две разные кнопки

Их путают чаще всего, хотя разница ровно в одном слове. proxy_cache_bypass запрещает брать ответ из кэша — запрос уходит на бэкенд, а результат при этом всё равно может быть сохранён. proxy_no_cache запрещает сохранять ответ в кэш — но если подходящая запись уже есть, она будет отдана. Поэтому для «этот пользователь не должен ни получать чужое, ни класть своё» нужны обе директивы с одинаковым условием. Одна без другой — половина решения, причём в обе стороны дырявая.

Условие я почти всегда строю через map, а не через голую переменную. Голый $cookie_PHPSESSID даёт срабатывание на любой сессионной куке, включая ту, что фреймворк выдаёт анонимному гостю, — и кэш выключается для всех, hit ratio падает в ноль, а вы неделю не понимаете, почему. map по regexp на куку именно авторизации (или на отдельную куку-флаг, которую приложение ставит только после логина) даёт предсказуемое поведение. Если приложение такой куки не ставит — попросите разработчиков её добавить, это пять строк и самый дешёвый способ сделать кэш безопасным.

Второй частый случай — принудительный обход кэша для отладки. Классический рецепт из документации — proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment — рабочий, но добавлю предостережение: параметр запроса типа ?nocache=1 в проде оставлять нельзя, иначе любой бот сможет прогревать ваш бэкенд напрямую. Оставляйте либо куку, которую ставите себе руками, либо ограничение по IP.

# обход кэша по служебной куке, которую ставлю себе руками
proxy_cache_bypass $cookie_nocache;
proxy_no_cache     $cookie_nocache;

# запросы с авторизацией не читаем из кэша и не пишем в него
proxy_cache_bypass $http_authorization;
proxy_no_cache     $http_authorization;

И про метод запроса, раз уж речь о кнопках. proxy_cache_methods по умолчанию содержит GET и HEAD, причём эти два метода кэшируются всегда и убрать их из списка нельзя. Добавить туда POST технически можно, и иногда это предлагают для тяжёлых поисковых запросов, но я так не делаю: ключ кэша по умолчанию не содержит тела запроса, а значит два разных POST на один URL схлопнутся в одну запись. Если очень нужно — придётся включать тело запроса в proxy_cache_key, что само по себе хрупко, или переписывать поиск на GET. Второе честнее и обычно быстрее по трудозатратам.

Проверьте свои условия на пустоту буквально: строка "0« считается ложью, а строка »00" — уже истиной. На переменных вида `$arg_nocache$arg_comment` это склейка, и она даёт неожиданные срабатывания.

Диагностика: как за пять минут понять, что происходит

Без $upstream_cache_status в логе разговор о кэше — это гадание. Первым делом я добавляю его в лог-формат и отдельным заголовком в ответ. Значений всего семь: MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED, HIT. Уже по их распределению видно диагноз: сплошной MISS — ответ не признан кэшируемым; сплошной BYPASS — переусердствовали с условиями; много EXPIRED при живом трафике — слишком короткий proxy_cache_valid или нет background update.

log_format cachelog '$remote_addr $status $upstream_cache_status '
                    '$request_time "$request" "$http_user_agent"';
access_log /var/log/nginx/cache.log cachelog;

Дальше — быстрый разбор по логу. Эти три команды я выполняю на каждом стенде, где жалуются на кэш:

# распределение статусов кэша за файл целиком
awk '{print $3}' /var/log/nginx/cache.log | sort | uniq -c | sort -rn

# топ URL, которые никогда не попадают в кэш
awk '$3=="MISS"{print $5,$6}' /var/log/nginx/cache.log | sort | uniq -c | sort -rn | head -20

# что реально отдаёт бэкенд, минуя nginx
curl -sSD- -o /dev/null -H 'Host: salon.example.com' http://127.0.0.1:8080/catalog/

И проверка на ту самую утечку, ради которой всё затевалось. Файл кэша хранит заголовки ответа бэкенда как есть, поэтому grep по каталогу кэша быстро показывает, попадали ли в кэш ответы с Set-Cookie. Если grep что-то нашёл и рядом нет proxy_hide_header Set-Cookie — куки из этих записей раздаются клиентам, и лечится это только очисткой кэша плюс инвалидацией сессий на бэкенде. Если proxy_hide_header стоит, находка в файлах ожидаема, и решающая проверка другая: запрос через nginx на закэшированный URL (статус HIT) не должен содержать Set-Cookie в ответе.

Ещё один тест, который я обязательно прогоняю перед сдачей, — параллельная нагрузка на холодный кэш. Смысл в том, чтобы убедиться, что proxy_cache_lock реально работает и десять одновременных запросов на непрогретый URL дают один поход на бэкенд, а не десять. Проверяется в одну строку: очищаем кэш, делаем reload, запускаем десяток параллельных curl на один URL и смотрим по логам апстрима, сколько запросов до него доехало. Если доехали все десять — либо proxy_cache_lock выключен, либо у вас разные ключи кэша из-за переменной, о которой вы забыли.

Очистка кэша на живом сервере: удаляйте содержимое, а не сам каталог — `rm -rf /var/cache/nginx/anon/*`. Запись, файл которой пропал, nginx обработает как промах и сходит на бэкенд, но ключи в keys_zone останутся до вытеснения — полностью сбросить их можно только `systemctl restart nginx`. Сам каталог nginx при старте пересоздаст, но права и SELinux-контекст придётся проверять заново. Директива proxy_cache_purge для точечной очистки есть только в коммерческой подписке.

Что делать в первую очередь, а на что можно забить

Приоритет номер один — убедиться, что в кэш не попадает ничего персонального. Это единственный пункт из всей темы, который стоит денег и репутации: утечка чужого кабинета — это уже разговор с юристами и, при персональных данных, с регулятором. Всё остальное — вопрос производительности, а её отсутствие никого не разоряет за один день. Поэтому порядок такой: сначала proxy_no_cache и proxy_cache_bypass по признаку авторизации, потом сужение location до анонимных маршрутов, и только потом тюнинг сроков и hit ratio.

Приоритет номер два — заставить бэкенд отдавать корректные заголовки. Это скучно, требует разработчиков и почти всегда откладывается, но именно здесь лежит правильное решение. Если приложение аккуратно ставит Cache-Control: public, max-age=600 на каталог и private, no-store на кабинет, то в nginx вам вообще не нужен ни один proxy_ignore_headers: он сам всё разложит правильно, а вы получите бонусом корректное поведение браузерных кэшей и CDN. Я всегда предлагаю сначала этот путь и только при отказе клиента строю обвязку на стороне nginx.

А вот на что можно забить спокойно. Не надо гнаться за hit ratio выше 90 % — на сайте с большим каталогом и длинным хвостом это недостижимо и не нужно, эффект даёт кэширование верхних 5 % URL. Не надо ставить огромные max_size «на вырост»: cache manager честно обходит записи, и на десятках миллионов файлов вы получите заметный I/O там, где его не ждали. Не надо ставить proxy_cache_min_uses больше 2–3 — при агрессивных значениях горячие страницы просто не успевают попасть в кэш между инвалидациями. И не надо изобретать своё сложное значение proxy_cache_key: чем больше в нём переменных, тем больше вариантов, и тем реже HIT.

И последнее, спорное. Многие включают proxy_cache_use_stale с широким списком http_5xx и оставляют навсегда. Я так делаю осознанно и считаю правильным для витрин и каталогов: отдать посетителю страницу пятиминутной давности лучше, чем 502. Но для API, где ответ — это данные, на которых принимаются решения, устаревший ответ хуже честной ошибки, потому что клиентское приложение не отличит их. Здесь я оставляю только error timeout updating и никаких http_5xx.

Если сводить всё к одному абзацу: nginx ведёт себя ровно так, как ему сказал бэкенд, и почти все «странности кэша» — это отражение заголовков, которые никто не смотрел. Соблазн заглушить эти заголовки одной директивой очень велик, потому что результат виден мгновенно. Но proxy_ignore_headers — это выключение защиты, а не включение производительности, и цена ошибки здесь несимметрична: выигрыш измеряется миллисекундами, а проигрыш — чужими персональными данными в чужом браузере. Начинайте с заголовков приложения, кэшируйте узко и обязательно тестируйте кэш двумя разными пользователями.

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

Частые вопросы

Я поставил proxy_cache_valid 200 1h, а в логе всё равно MISS. Что смотреть первым?

Заголовки ответа бэкенда, а не конфиг nginx. Выполните curl -sSD- -o /dev/null напрямую к апстриму и ищите Set-Cookie, Cache-Control: private/no-cache/no-store, Vary: *, X-Accel-Expires: 0. Любого из них достаточно, чтобы nginx отказался кэшировать — параметры кэширования из заголовков ответа имеют приоритет над proxy_cache_valid.

Можно ли просто написать proxy_ignore_headers Cache-Control Set-Cookie и не мучиться?

Технически можно, практически — это самый частый способ выдать одному пользователю сессию другого. Директива отключает обработку заголовка, но не удаляет его из ответа: Set-Cookie сохраняется в кэше вместе с телом и раздаётся всем, кто попал в ту же запись по ключу. Если без игнорирования никак — обязательно добавляйте рядом proxy_hide_header Set-Cookie и применяйте это только в узком location с заведомо анонимным контентом.

Чем отличаются proxy_no_cache и proxy_cache_bypass?

proxy_cache_bypass запрещает брать ответ из кэша (запрос уходит на бэкенд), proxy_no_cache запрещает сохранять ответ в кэш. Это независимые кнопки, и для авторизованных пользователей нужны обе с одинаковым условием. Обе срабатывают, если хотя бы один их параметр непустой и не равен строке «0».

Почему после proxy_ignore_headers Cache-Control кэш всё равно не заработал?

Потому что вместе с обработкой Cache-Control вы отключили и источник информации о времени жизни ответа. Если Expires и X-Accel-Expires тоже нет, nginx не знает, на сколько кэшировать, и не кэширует. Ровно об этом писал Максим Дунин в рассылке nginx: после игнорирования Cache-Control нужно явно задать proxy_cache_valid.

Как включить кэш только для неавторизованных, если приложение всем ставит сессионную куку?

Через map по куке, которая появляется только после логина, и связку proxy_cache_bypass/proxy_no_cache с этой переменной. Если такой куки нет — попросите разработчиков добавить отдельную куку-флаг после успешной аутентификации, это несколько строк кода и самый надёжный способ разделить контуры. Ключ кэша при этом персонализировать не надо: это раздувает кэш и почти не даёт HIT.

Как проверить, что в кэш не утекли чужие куки?

grep -rl 'Set-Cookie' /var/cache/nginx/ — если вывод пуст, ответы с куками в кэш не попадали. Если grep что-то нашёл, проверьте через nginx закэшированный URL: в HIT-ответе не должно быть Set-Cookie (при proxy_hide_header Set-Cookie поле остаётся в файле, но клиенту не передаётся). Если кука уходит клиентам — уберите кэш с этого location, очистите каталог кэша, перезапустите nginx и инвалидируйте сессии на стороне приложения: закэшированные куки остаются валидными до истечения сессии, поэтому одной очисткой кэша дело не заканчивается.

Какая версия nginx актуальна в 2026 году?

На сентябрь 2026 stable — 1.30.4, mainline — 1.31.5; ветки 1.28.x и 1.26.x переведены в legacy. Логика кэширования, описанная в статье, одинакова для всех этих веток: обработка Vary работает с 1.7.7, min_free в proxy_cache_path — с 1.19.1. Дистрибутивные пакеты обычно сильно старше, но на поведение кэша это не влияет.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи