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 и делает систему заметно спокойнее под пиками.