В продакшене нам нужен nginx не просто как обратный прокси, а как защитный барьер, который удержит фронт при сбоях апстримов. Ключ к этому — грамотная работа с error_page: безопасные фолбэки (fallback), единая политика на 5xx/тайм-ауты и даже примитивный circuit breaker на уровне прокси без внешних компонентов. Разберёмся, как связать error_page с proxy_intercept_errors, proxy_next_upstream, тайм-аутами и кешем, чтобы повысить устойчивость и предсказуемость отказов.
Зачем перехватывать ошибки через error_page
По умолчанию nginx просто прокидывает статус апстрима. Это честно, но опасно для UX и безопасности: стек-трейсы приложения попадают пользователю, «шумные» коды меняются от запроса к запросу, а страницы бэкапа выглядят по-разному в зависимости от того, где упало. Используя error_page, мы:
- Создаём единый контролируемый ответ при сбоях: статус, заголовки, текст или JSON.
- Избегаем утечек внутренних деталей приложения и стеков.
- Можем возвращать безопасный контент (статический HTML или предсобранный JSON) без обращения к падающему апстриму.
- Фиксируем договорённости с клиентами:
503сRetry-Afterпри техработах,200с «degraded» для tolerant-клиентов и т. д.
Принцип: прокси должен деградировать предсказуемо. Пользователь должен видеть стабильный ответ, а не рулетку из 502/504/500 и случайных тел.
Как работает связка proxy_intercept_errors + error_page
Перехват кодов, пришедших от апстрима, включается директивой proxy_intercept_errors on;. Далее правило error_page перенаправляет обработку в заранее подготовленную «безопасную» локацию.
location / {
proxy_pass http://app;
proxy_intercept_errors on;
error_page 500 502 503 504 = @fallback;
}
location @fallback {
internal;
add_header Cache-Control "no-store" always;
add_header X-Proxy-Mode "fallback" always;
return 503 "Service temporarily unavailable. Please try again later.";
}
Ключевые нюансы:
internal;запрещает прямой вызов локации извне.returnили отдача статического файла исключают обращение к аппликации.- Если нужно сохранить код ответа — используйте форму
=503 @fallbackвerror_page. - Во избежание зацикливаний оставьте
recursive_error_pages off;(по умолчанию).
Базовые фолбэки: HTML и API
HTML-сайт
Для публичного сайта часто достаточно статической страницы «Технические работы» плюс правильные заголовки. Положите подготовленную страницу на диск и переиспользуйте её во всех серверных блоках.
location @fallback_html {
internal;
root /var/www/fallbacks;
add_header Cache-Control "no-store" always;
add_header Retry-After "120" always;
try_files /maintenance.html =503;
}
Такой подход гарантирует единообразие и минимальную латентность при сбоях.
API-сценарий
Клиенты API зачастую готовы к «degraded mode». Мы можем вернуть 200 с простым JSON, чтобы не ломать обработку на стороне клиента, но с явным признаком деградации.
location /api/ {
proxy_pass http://api_upstream;
proxy_intercept_errors on;
error_page 502 504 = @api_fallback;
}
location @api_fallback {
internal;
default_type application/json;
add_header X-Proxy-Mode "degraded" always;
return 200 '{"status":"degraded","message":"temporary upstream failure"}';
}
Если же важно сохранять семантику отказа для ретраев — верните 503 с Retry-After.
Circuit breaker на уровне прокси
В терминологии сетевых паттернов circuit breaker — это отсечка «плохого» апстрима и быстрый ответ без попыток бесконечно стучаться в неисправный сервис. В nginx мы строим этот паттерн из нескольких кирпичиков:
- Границы времени: короткие, осмысленные тайм-ауты на соединение и чтение (
proxy_connect_timeout,proxy_read_timeout,proxy_send_timeout). - Политика ретраев:
proxy_next_upstreamс ограничениямиproxy_next_upstream_triesиproxy_next_upstream_timeout. - Маркировка и исключение «битых» бэкендов:
max_fails,fail_timeout, опциональноslow_startи общийzoneдля шаринга состояния. - Фолбэк через
error_page, если все попытки исчерпаны.
upstream app {
zone app_zone 64k;
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.13:8080 backup;
}
server {
# тайм-ауты и границы
proxy_connect_timeout 1s;
proxy_read_timeout 2s;
proxy_send_timeout 2s;
location / {
proxy_pass http://app;
proxy_intercept_errors on;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 3s;
error_page 500 502 503 504 = @fallback_html;
}
}
Здесь мы не включаем параметр non_idempotent — это значит, что POST/PATCH/DELETE по умолчанию не будут ретраиться на другой бэкенд, что безопаснее для операций с побочными эффектами. Для GET/HEAD ретраи разрешены. Когда попытки исчерпаны, включается error_page и отдаётся локальный фолбэк.
Плавное восстановление
Если вы возвращаете узел после падения, полезен slow_start в апстриме — он постепенно наращивает вес сервера, чтобы не обрушить его лавиной трафика сразу после восстановления.
upstream app {
zone app_zone 64k;
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s slow_start=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s slow_start=10s;
}
Старый контент как fallback: use_stale
Фолбэк не всегда должен быть «плоским». Если вы используете кеш на прокси, можно вернуть «старый, но годный» ответ при ошибке апстрима. Это хороший компромисс для контента, который допустимо показывать с задержкой.
proxy_cache mycache;
proxy_cache_valid 200 301 302 10m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
Схема: при сбое апстрима nginx отдаёт копию из кеша, а в фоне может пытаться обновить её. В связке с error_page это даёт многоступенчатый план: сначала «stale», затем статический фолбэк, если кеша нет.

Maintenance без хаоса
Плановые работы — частный случай управляемой деградации. На входе: 503 всем, плюс Retry-After. Удобно сделать «переключатель» через отдельный конфиг или флаг-файл, чтобы не трогать основную логику.
# include /etc/nginx/flags/maintenance.conf;
# внутри файла может быть: set $maintenance 0; или set $maintenance 1;
map $maintenance $maintenance_on {
default 0;
1 1;
}
server {
if ($maintenance_on) {
return 503;
}
error_page 503 = @maintenance;
location @maintenance {
internal;
add_header Retry-After "120" always;
root /var/www/fallbacks;
try_files /maintenance.html =503;
}
}
Так мы не ломаем апстрим, а управляем статусом на уровне прокси. В журналах это будет видно как 503 с нулевым временем апстрима. Для бэкенда БД на время техработ пригодится материал перевод MySQL в read-only на время работ.
Гигиена заголовков и отсутствие утечек
Фолбэк — это ещё и точка санитизации ответов: не допускайте вытекания заголовков апстрима и метаданных окружения. В фолбэк-локациях задавайте нужные типы (default_type), добавляйте контролируемые Cache-Control/Retry-After и избегайте переноса случайных заголовков с апстрима.
Не перехватывайте 4xx (например, 404) у приложений, если у них есть собственная логика. Для API это особенно важно, чтобы не ломать контракт.
Логирование: что и как смотреть
Для разбора «почему сработал фолбэк» пригодится расширенный формат логов с полями апстрима.
log_format upstream_ext '$remote_addr - $request $status '
'up=$upstream_addr us=$upstream_status '
'urt=$upstream_response_time uct=$upstream_connect_time '
'rt=$request_time mode=$sent_http_x_proxy_mode';
access_log /var/log/nginx/access.log upstream_ext;
Проверяйте, что upstream_status и upstream_response_time осмысленны. Если mode=degraded или mode=fallback, значит, отработал наш барьер. upstream_connect_time поможет отличить сетевые проблемы от неповоротливого приложения.

Тестирование отказов
Перед выкатыванием проверьте локально ключевые сценарии: тайм-аут соединения, медленный ответ, 502/504, исчерпание ретраев.
# ограничьте общее время запроса клиента
curl -I --max-time 2 https://example.test/
# симулируйте медленный апстрим (например, через tc/netem) и смотрите, как срабатывает fallback
# отключите один бэкенд и проверяйте трассировку в логах по $upstream_addr
Если нужен изолированный стенд для проверки сценариев деградации и кеширования, поднимите его на VDS, чтобы не рисковать боевой инфраструктурой.
Ограничение каскадов и защита от штормов
Без ограничений ретраи и длинные тайм-ауты превращают отказ апстрима в лавинообразную деградацию. Держите параметры консервативными: небольшие тайм-ауты, ограничение количества и общего времени ретраев. На горячем трафике включайте keepalive в апстриме и разумные буферы, чтобы не тратить время на каждый коннект. В точке фолбэка выбирайте лёгкую отдачу (короткий текст или небольшой HTML).
Чек-лист безопасного fallback и circuit breaker
- Включить
proxy_intercept_errors on;там, где нужна политика на 5xx/тайм-ауты. - Определить
error_pageдля500 502 503 504и указывать именованную локацию сinternal. - На уровне апстрима задать
zone,max_fails,fail_timeout, опциональноslow_start. - Для ретраев:
proxy_next_upstream,proxy_next_upstream_tries,proxy_next_upstream_timeout. Не добавлятьnon_idempotent, если не понимаете риски. - Тайм-ауты: короткие
proxy_connect_timeout,proxy_read_timeout,proxy_send_timeoutв зависимости от SLA. - Опционально кеш и
proxy_cache_use_staleдля «старого, но годного» ответа. - Фолбэк для HTML и API держать раздельно (тип ответа, заголовки, тела).
- Логи с полями
$upstream_status,$upstream_response_time,$upstream_addrи признаком режима. - Автоматический maintenance-режим:
503+Retry-Afterчерез флаг/инклюд.
Типичные ошибки и подводные камни
- Забыли
proxy_intercept_errors:error_pageне сработает для проксированных ответов. - Перехватывают 404/401 у приложения: ломает контракт API и UX веб-приложений.
- Возврат больших фолбэк-страниц: увеличивает латентность и расход CPU/памяти при массовых отказах.
- Слишком щедрые тайм-ауты и бесконечные ретраи: усиливает шторм; ставьте верхние границы.
- Прямой доступ к фолбэк-локации: отсутствие
internalпозволяет дергать её извне. - Зацикливание error_page: явно задавайте код через
=503и держитеrecursive_error_pages off. - Случайное кеширование ошибок: ставьте
Cache-Control: no-storeна фолбэк-ответы, если не хотите их кешировать.
Итог
Одной директивой error_page проблему отказоустойчивости не решить, но в сочетании с proxy_intercept_errors, грамотными тайм-аутами, политикой ретраев и опциональным кешем она превращается в мощный защитный барьер. Вы получаете контролируемые ответы, предсказуемую деградацию и простейший circuit breaker на уровне nginx без дополнительного софта. Начните с базового фолбэка на 5xx/тайм-ауты, разделите HTML и API-сценарии, добавьте «stale» для контента и доведите параметры так, чтобы при падении апстрима пользователи видели стабильный и безопасный ответ.


