API, которые ходят к внешним бэкендам (SaaS, микросервисы в другом кластере, сторонние сервисы), часто упираются в сеть, квоты и холостые повторные запросы. Грамотный reverse proxy на Nginx с proxy_cache, stale‑while‑revalidate и cache lock резко снижает латентность и защищает бэкенд от «бурстов». Ниже — практические схемы и конфиги для продакшна.
Когда кэшировать API — и когда нельзя
Кэширование хорошо работает для идемпотентных запросов (GET, HEAD) и ответов, которые зависят только от публичных параметров (путь, query, заголовки согласования). Если в ответе присутствуют персональные данные пользователя или привязка к сессии, кэшировать это без сегментации ключа недопустимо. Минимальные правила:
- Кэшируем только
GET/HEAD;POST— по умолчанию не кэшируем. - Если приходит
Authorizationили есть авторизационные куки — кэш пропускаем. - Ответы с
Set-Cookieне кладем в кэш (или аккуратно скрываем этот заголовок и сегментируем ключ, если это не персональные данные). - Уважаем
Cache-Control/Expires,ETag,Last-Modified,Vary.
Если API возвращает персональные данные, а вариации определяются токеном или cookie, лучше пропускать кэш. Альтернатива — строгая сегментация ключа по токену, что фактически превращает кэш в per-user и редко оправдано.
Базовая архитектура proxy_cache
Кэш в Nginx состоит из зоны метаданных в памяти (keys_zone) и файлов на диске. Минимальная настройка: путь, размер, уровни каталогов, неактивность и лимиты загрузчика. Если разворачиваете прокси на отдельном сервере — удобно взять VDS и выделить быстрый диск под каталог кэша.
# http {}
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:200m max_size=20g inactive=1h use_temp_path=off loader_threshold=300 loader_files=200;
Пояснения по параметрам:
levels=1:2— разбиение кеш-файлов на подпапки, чтобы избежать огромных директорий.keys_zone=api_cache:200m— индекс в памяти для быстрых lookup. 200 МБ обычно достаточно для десятков миллионов ключей, но зависит от длины ключей.max_size— лимит суммарного размера кэша на диске.inactive— срок, после которого неиспользуемые элементы удаляются, даже если их TTL ещё не истёк.loader_threshold/loader_files— фоновая подгрузка индекса при старте.use_temp_path=off— безопаснее и быстрее писать сразу в каталог кэша.
Серверный блок: минимальная рабочая конфигурация
server {
listen 80;
server_name api.example.local;
# Ключ кэша: метод + хост + URI + query
set $cache_key "$request_method|$host|$request_uri";
location / {
proxy_pass http://backend_api;
proxy_cache api_cache;
proxy_cache_key $cache_key;
proxy_cache_valid 200 301 404 10m;
proxy_cache_valid any 1m;
proxy_cache_background_update on; # stale‑while‑revalidate
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_cache_lock on; # защита от thundering herd
proxy_cache_lock_age 60s;
proxy_cache_lock_timeout 10s;
proxy_cache_revalidate on; # 304 по ETag/Last-Modified
proxy_buffering on;
proxy_buffers 16 32k;
proxy_busy_buffers_size 64k;
proxy_connect_timeout 3s;
proxy_read_timeout 15s;
proxy_send_timeout 15s;
add_header X-Cache-Status $upstream_cache_status always;
}
}
Эта базовая схема уже даёт: кэширование успешных ответов, фоновые обновления при протухании, раздачу stale при сбое бэкенда и блокировку запросов‑дубликатов на холодном промахе.

Контроль ключа кэша: чтобы не кэшировать приватное
Главная ошибка — общий ключ для всех пользователей. В API часто приходит Authorization: Bearer ..., сессионные куки, локаль. Покажем безопасную матрицу пропуска кэша:
# http {}
map $request_method $cache_method_ok {
default 0;
GET 1;
HEAD 1;
}
map $http_authorization $auth_present {
default 1; # любой непустой — есть авторизация
"" 0;
}
map $cookie_session $sess_present {
default 1; # любая сессия — считаем приватным
"" 0;
}
# 0 — кэш разрешен; 1 — пропуск кэша
map "$cache_method_ok$auth_present$sess_present" $skip_cache {
default 1;
"100" 0; # GET/HEAD без авторизации и без сессии
}
location /api/ {
proxy_pass http://backend_api;
proxy_cache api_cache;
proxy_cache_key "$request_method|$host|$request_uri";
proxy_no_cache $skip_cache;
proxy_cache_bypass $skip_cache;
# не кэшировать, если апстрим прислал Set-Cookie
map $upstream_http_set_cookie $no_cache_setcookie {
default 1;
"" 0;
}
proxy_no_cache $no_cache_setcookie;
proxy_cache_valid 200 301 404 10m;
proxy_cache_background_update on;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_age 60s;
proxy_cache_lock_timeout 10s;
proxy_cache_revalidate on;
add_header X-Cache-Status $upstream_cache_status always;
}
Ключ кэша можно обогащать, если API зависит, например, от локали: добавьте |$http_accept_language или вводите явный параметр lang и включите query в ключ (в примерах он уже учтен). Заголовок Vary Nginx учитывает: кэширующая прослойка хранит отдельные варианты, если апстрим вернул Vary.
TTL: откуда он берется и как им управлять
Есть два пути:
- Доверять апстриму: он присылает
Cache-Control: s-maxage=N, stale-while-revalidate=M, stale-if-error=Kи/илиExpires. Тогда можно не задаватьproxy_cache_validили оставить только fallbackany. - Принудительно задавать TTL на прокси: через
proxy_cache_validдля кодов 200/301/404 и резервный на «любой» ответ.
В API удобно совмещать: уважать заголовки апстрима, но иметь дефолтные TTL на случай их отсутствия. Если вы получаете из внешнего сервиса агрессивные no-store, а ваш кейс допускает кэш — можно выборочно игнорировать:
# Осторожно: осознанно переопределяем политику апстрима
proxy_ignore_headers Cache-Control Expires;
proxy_cache_valid 200 1m;
Избыточное игнорирование
Cache-Controlчревато устаревшими данными. Применяйте только для безопасных публичных эндпоинтов (справочники, списки, конфигурации, которые редко меняются).
stale‑while‑revalidate: быстрые ответы при протухшем кэше
Паттерн «отдаём устаревшее, но сразу в фоне обновляем» перекрывает пики и резкие всплески трафика. В Nginx это включается двумя настройками:
proxy_cache_background_update on;— при просроченном объекте ответа клиенту отдаётся текущая копия, а обновление выполняется в фоне.proxy_cache_use_stale updating;— разрешает использовать устаревшую копию, пока идёт обновление.
Если апстрим присылает Cache-Control: stale-while-revalidate=30, Nginx будет в эти 30 секунд после истечения основного TTL раздавать копию, параллельно инициализируя обновление. Даже без этого заголовка вы можете имитировать поведение, подобрав TTL и включив фоновые апдейты.
Cache lock: защита от шторма холодных промахов
Проблема «thundering herd»: первый промах тянет за собой сотни одинаковых запросов к бэкенду. proxy_cache_lock делает так, что только один запрос идёт к апстриму, остальные ждут либо его результат, либо таймаут:
proxy_cache_lock on;
proxy_cache_lock_timeout 10s; # сколько ждать лидера
proxy_cache_lock_age 60s; # не ставить лок на объекты старше указанного возраста
Комбинация cache lock + stale‑while‑revalidate закрывает оба кейса: холодные промахи и горячие апдейты.
stale‑if‑error: устойчивость при сбоях бэкенда
Когда апстрим отвечает ошибкой или падает по таймауту, отдавать пользователю пустой ответ — плохая идея. Включаем выдачу последней копии:
proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504 updating;
Так вы выровняете SLA даже при кратковременных авариях на стороне внешнего сервиса.
Revalidate по ETag/Last‑Modified: экономим трафик и CPU апстрима
Если апстрим умеет ETag и/или Last-Modified, Nginx может слать условные запросы (If-None-Match/If-Modified-Since) вместо полного перезапроса. Это включает proxy_cache_revalidate on;. При 304 кэш продлевает TTL значительно дешевле полного рендеринга на стороне бэкенда.
Продвинутый пример: публичное API и приватные эндпоинты
upstream backend_api {
server 10.0.0.10:8080;
keepalive 64;
}
map $request_method $cache_method_ok {
default 0;
GET 1;
HEAD 1;
}
map $http_authorization $auth_present { default 1; "" 0; }
map $cookie_session $sess_present { default 1; "" 0; }
map "$cache_method_ok$auth_present$sess_present" $skip_cache { default 1; "100" 0; }
server {
listen 80;
server_name api.example.local;
# Публично-агрегирующие эндпоинты (кэшируем агрессивно)
location /v1/catalog/ {
proxy_pass http://backend_api;
proxy_cache api_cache;
proxy_cache_key "$request_method|$host|$request_uri";
proxy_no_cache $skip_cache;
proxy_cache_bypass $skip_cache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_background_update on;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 8s;
proxy_cache_lock_age 60s;
proxy_cache_revalidate on;
add_header X-Cache-Status $upstream_cache_status always;
}
# Приватные эндпоинты — без кэша
location /v1/user/ {
proxy_pass http://backend_api;
proxy_no_cache 1;
proxy_cache_bypass 1;
}
}
Такое разделение часто покрывает 80% трафика кэшем и оставляет чувствительные части API нетронутыми.
Особенности API: POST, тело запроса, GraphQL
Технически Nginx умеет кэшировать POST при указании proxy_cache_methods GET HEAD POST. Но это безопасно только если серверная семантика действительно идемпотентна и контент определяется строго телом запроса. Тогда ключ кэша нужно расширить, например, хешом тела:
proxy_cache_methods GET HEAD POST;
set $req_body_hash "";
if ($request_method = POST) { set $req_body_hash $request_body; }
proxy_cache_key "$request_method|$host|$request_uri|$req_body_hash";
Минусы: рост RAM/CPU на прокси, потенциальные лимиты на размер тела, сложнее отлаживать. Для большинства API проще оставить кэш только для GET/HEAD.

Границы ответственности: Vary, компрессия, клиентские директивы
Nginx корректно поддерживает Vary, храня отдельные варианты. Если вы включили gzip/brotli, убедитесь, что клиенты получают совместимый вариант (обычно достаточен gzip_vary on; и согласованный набор компрессии на прокси и апстриме). Подробно о нюансах статики, WebP/AVIF и заголовков Vary — смотрите материал здесь.
Клиентские заголовки типа Cache-Control: no-cache касаются клиентских кэшей; на прокси они не обязаны сбрасывать серверный кэш — это решается вашими правилами proxy_cache_bypass.
Надёжность и таймауты к внешним бэкендам
В кэширующем прокси важно выставить честные таймауты и стратегию переключений, иначе кэш не спасёт от хрупкости апстрима. Практика:
proxy_connect_timeout 2–5s,proxy_read_timeout 10–20sдля API;proxy_next_upstream error timeout http_500 http_502 http_503 http_504при работе с пулом апстримов;keepaliveв апстриме, чтобы снизить TLS/три рукопожатия.
Кэш — не замена ретраям и таймаутам. Он сглаживает пик и экономит апстрим, но не чинит логические ошибки на бэкенде.
Инвалидация: как жить без purge
В open-source Nginx нет штатного purge по ключу. Рабочие варианты:
- Версионирование URI: добавляете параметр
?v=timestampили инкремент версии при релизах. - Короткие TTL +
revalidate: быстрые апдейты через условные запросы. - Раздельные зоны кэша под разные сервисы и удаление каталога целиком вне пиков (осторожно и осознанно).
Если нужен точечный purge по тегам/ключам — закладывайте это в архитектуру API (контент‑версии, ключи по бизнес‑объекту, отдельные namespace в proxy_cache_path).
Наблюдаемость: метрики и отладка
Минимум — заголовок X-Cache-Status (MISS/HIT/EXPIRED/STALE/UPDATING и т.д.). Для аналитики удобно вынести отдельный log_format с $upstream_cache_status, временем ответа и кодом. В тестах пользуйтесь curl -I и инструментами нагруженного тестирования, чтобы увидеть эффект background update и cache lock.
Типовые пресеты для API
- Публичные справочники:
proxy_cache_valid 200 10m,background_update on,use_stale updating error timeout. - Каталоги/поиск с частыми обновлениями:
200 1m+revalidate on+ довериеETag. - Персональные данные:
proxy_cache_bypass 1всегда. - Внешние SaaS с квотами: короткие TTL, агрессивный
stale-if-error, честные таймауты,cache lock.
Чек-лист перед продом
- Определите кэшируемые эндпоинты и явно запретите кэш там, где риск утечки данных неприемлем.
- Согласуйте TTL: от апстрима (заголовки) или от прокси (директивы), либо гибрид.
- Включите
proxy_cache_background_update,proxy_cache_use_stale updating,proxy_cache_lock— это три кита «умного» кэша. - Проверьте
ETag/Last-Modifiedи включитеproxy_cache_revalidate. - Убедитесь, что ответы с
Set-Cookieне попадают в кэш. - Добавьте
X-Cache-Statusи графики по кэш-хитам/промахам.
Итог
Умный кэш Nginx для API — это не только proxy_cache и TTL. Это связка фоновых апдейтов (stale‑while‑revalidate), устойчивость на ошибках (stale‑if‑error через use_stale), защита от шторма холодных промахов (cache lock) и корректная сегментация ключей. На практике такая конфигурация даёт 5–20x разгрузку внешних бэкендов, сокращает p95/p99 и делает систему заметно спокойнее под пиками.


