Что делать, если

Ошибка 502 Bad Gateway: что она значит, кто её отдаёт и как исправить

Ошибка 502 Bad Gateway: что делать администратору — кто отдаёт 502, как найти причину по error.log nginx и php-fpm за десять минут и узнать о ней раньше клиентов.

502 Bad Gateway означает, что сервер, который принял ваш запрос, сам ничего с ним не делает, а передаёт дальше — и с этого «дальше» получил либо обрыв соединения, либо ответ, который не смог разобрать как HTTP. Так это определяет RFC 9110: код 502 отдаёт шлюз или прокси, а не само приложение. Практический вывод: nginx, Apache, Cloudflare или балансировщик живы, а вот то, что стоит за ними — php-fpm, Node, gunicorn, сервер-origin, — не отвечает или отвечает мусором. Чинить нужно там, а не в прокси.

Четыре команды, с которых стоит начинать почти всегда: curl -I к сайту (кто именно отдал 502), tail /var/log/nginx/error.log (что nginx не понравилось в upstream), systemctl status php8.2-fpm (жив ли бэкенд) и ss -ltnp (слушает ли он тот порт или сокет, куда nginx стучится). Ниже — как читать вывод каждой из них, какие причины встречаются чаще всего и что менять в конфиге.

Не путайте 502 с соседями. 503 Service Unavailable — сервер сам сообщает, что перегружен или на обслуживании: nginx отдаёт его при срабатывании limit_req/limit_conn, а приложение — когда включён режим maintenance. 504 Gateway Timeout — соединение с бэкендом установилось, но ответ не пришёл за отведённое время (proxy_read_timeout, fastcgi_read_timeout). А 502 — бэкенд отверг соединение (Connection refused, нет сокета, нет прав), оборвал его до полного ответа или ответил не-HTTP. Если же соединение просто не успело установиться за proxy_connect_timeout — это тоже 504.

Кто именно отдал 502

Между браузером и вашим кодом может стоять три-четыре слоя, и каждый умеет генерировать 502 самостоятельно: Cloudflare (или другой CDN), балансировщик у хостера, nginx или Apache как reverse proxy, и только потом php-fpm или приложение. Первый шаг диагностики — понять, на каком слое запрос умер. Это видно по заголовку Server и телу ответа:

curl -s -o /tmp/body.html -D - https://example.ru/ ; head -c 400 /tmp/body.html
Кто отдалКак выглядитЧто это значит
nginxServer: nginx, тело <center>nginx/1.24.0</center> (при server_tokens off — просто nginx)nginx не смог поговорить с proxy_pass/fastcgi_pass
Apache (mod_proxy)тело «Proxy Error… invalid response from an upstream server»то же самое, но для ProxyPass
CloudflareServer: cloudflare, заголовок cf-ray, страница с Ray IDorigin принял соединение, но ответил не-HTTP или оборвал его
HAProxyтело «The server returned an invalid or incomplete response»бэкенд оборвал ответ, чаще всего из-за некорректных заголовков
Облачный балансировщикServer: awselb/2.0 и подобные, короткий стандартный HTML без версииtarget закрыл или сбросил соединение либо ответил невалидным HTTP; если ни один target не прошёл health-check, ALB отдаст 503, а не 502

Если тело ответа — брендированная страница вашего приложения с кодом 502, это редкий случай, когда 502 отдало само приложение (например, оно тоже кому-то проксирует). Посмотреть заголовки ответа снаружи, не с самого сервера, — в том числе Server и cf-ray — можно через проверку HTTP-заголовков: запрос уходит с точки в России и в Европе, и сразу видно, отдаётся ли 502 одинаково или ошибка только с части сетей. Тело страницы ошибки смотрите curl’ом.

Когда впереди стоит Cloudflare, обязательно проверьте origin в обход него: возьмите реальный IP сервера и попросите curl подставить его вместо DNS.

curl -sI --resolve example.ru:443:203.0.113.10 https://example.ru/

Если на origin стоит сертификат Cloudflare Origin CA, добавьте -k: curl ему не доверяет, и это нормально.

Если origin отвечает 200, а через Cloudflare — ошибка, смотрите на код и на страницу. 521 — Cloudflare не смог открыть TCP-соединение: фаервол режет IP-диапазоны Cloudflare или сервер слушает не тот порт; 522 — соединение висит (таймаут); 525/526 — проблема с TLS на origin; 524 — origin молчит дольше 100 секунд. Брендированная страница Cloudflare с кодом 502 означает, что origin принял соединение, но ответил не-HTTP или оборвал его посреди ответа. Если же Server: cloudflare, а тело — стандартная страница nginx, значит 502 отдал сам origin, и Cloudflare ни при чём: идите на сервер.

Читаем error.log: шесть типовых причин

Практически каждая причина 502 в связке nginx + бэкенд оставляет в error.log характерную строку. По ней причина определяется до того, как вы откроете конфиг.

tail -n 100 /var/log/nginx/error.log | grep -E 'upstream|connect\(\)'
Строка в error.logПричинаЧто делать
connect() failed (111: Connection refused) while connecting to upstreamбэкенд не запущен или слушает другой портsystemctl status, ss -ltnp
connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory)путь к сокету в nginx не совпадает с listen в пуле php-fpmсверить fastcgi_pass и www.conf
connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied)сокет создан не тем пользователемlisten.owner/listen.group в пуле
connect() to unix:… failed (11: Resource temporarily unavailable)все воркеры php-fpm заняты, очередь listen.backlog переполненаподнять pm.max_children, найти медленные запросы
upstream prematurely closed connection while reading response headerворкер умер посреди запроса: OOM-killer, segfault, request_terminate_timeoutdmesg, лог php-fpm
upstream sent too big header while reading response header from upstreamзаголовки ответа не влезают в fastcgi_buffer_size/proxy_buffer_sizeувеличить буфер до 32k

Последний случай легко пропустить: сайт работает, а 502 ловят только авторизованные пользователи с большими cookie или страницы с длинным Set-Cookie от плагинов. Лечится двумя строками в location:

fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;

Для proxy_pass аналогично — proxy_buffer_size и proxy_buffers, описание параметров есть в документации nginx.

Пошаговая диагностика на сервере

Порядок важен: сначала выясняем, жив ли бэкенд, потом — слушает ли он там, куда ходит nginx, и только потом лезем в производительность.

Шаг 1. Состояние службы

systemctl status php8.2-fpm nginx --no-pager
journalctl -u php8.2-fpm -n 50 --no-pager

active (running) у nginx и failed или inactive у php-fpm — самый частый сценарий: после обновления PHP старый пакет php8.1-fpm удалён или его служба остановлена, а в fastcgi_pass по-прежнему указан /run/php/php8.1-fpm.sock. Либо запустите нужный юнит (systemctl enable --now php8.2-fpm), либо переключите путь в nginx на актуальный сокет. В journalctl при этом видно причину, если php-fpm не стартует: синтаксическая ошибка в www.conf, занятый порт, отсутствие каталога /run/php.

Шаг 2. Кто что слушает

ss -ltnp | grep -E ':(80|443|8080|9000|3000)\b'
ss -xlp | grep -E 'php|fpm|sock'
grep -rhE 'fastcgi_pass|proxy_pass' /etc/nginx/ | sort -u

Первая команда показывает TCP-порты, вторая — unix-сокеты, третья — куда ходит nginx. Три вывода должны сходиться: если nginx проксирует на 127.0.0.1:3000, в ss должна быть строка с этим портом и именем процесса. Типичная нестыковка: приложение слушает 0.0.0.0:3000 внутри Docker-контейнера, а на хосте порт не проброшен, или Node запущен на localhost:3001 после смены переменной PORT.

Шаг 3. Память и OOM-killer

free -m
journalctl -k -n 200 --no-pager | grep -iE 'out of memory|killed process'

Строка вроде Out of memory: Killed process 18342 (php-fpm8.2) total-vm:412300kB объясняет 502, которые «появляются под нагрузкой и сами проходят». Ядро убило воркер посреди ответа, nginx получил обрыв и отдал 502. Обычно это следствие завышенного pm.max_children относительно реальной памяти сервера, реже — утечки в приложении или соседа на том же сервере (MySQL, поисковый индекс).

Шаг 4. Воспроизведение в обход nginx

Для приложения на TCP-порту достаточно curl -i http://127.0.0.1:3000/. Для php-fpm на сокете — cgi-fcgi из пакета libfcgi-bin:

SCRIPT_NAME=/index.php SCRIPT_FILENAME=/var/www/site/public/index.php REQUEST_METHOD=GET \
  cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock

Если бэкенд напрямую отвечает нормально, а через nginx — 502, проблема в связке (сокет, права, буферы). Если и напрямую ошибка — проблема в приложении, и nginx можно пока не трогать.

PHP-FPM не успевает: считаем pm.max_children

Ошибка (11: Resource temporarily unavailable) в nginx и строка [pool www] server reached pm.max_children setting (5), consider raising it в /var/log/php8.2-fpm.log — это не «php-fpm сломался», а «воркеров меньше, чем одновременных запросов». Дефолтных пяти воркеров хватает для визитки, но не для магазина на WordPress с кешем, который сбрасывается по крону.

Число воркеров считается от памяти, а не берётся с потолка. Сначала смотрим, сколько реально ест один воркер после прогрева:

ps -ylC php-fpm8.2 --no-headers --sort:rss | awk '{sum+=$8; n++} END {print sum/n/1024 " MB в среднем на воркер"}'

В выборку попадёт и мастер-процесс — он мелкий и оценку почти не сдвигает. Допустим, вышло 60 МБ, а сервер с 4 ГБ, из которых MySQL и nginx занимают 1,5 ГБ. Остаётся около 2,5 ГБ, делим на 60 МБ — примерно 40 воркеров, с запасом ставим 30–35:

; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 32
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 500
pm.status_path = /fpm-status
request_terminate_timeout = 60s

pm.max_requests перезапускает воркер после 500 запросов и гасит медленные утечки, pm.status_path даёт страницу с числом активных воркеров и длиной очереди (закройте её в nginx по IP). Описание всех параметров пула — в документации PHP. Если после расчёта воркеров всё равно не хватает, увеличивать число дальше бессмысленно — они упрутся в память или в базу; ищите, почему запрос занимает воркер на секунды: медленные SQL-запросы, внешние HTTP-вызовы без таймаута, отсутствие opcache.

Таймауты: где 502, а где 504

С таймаутами постоянно возникает путаница, потому что один и тот же долгий запрос даёт разные коды в зависимости от того, кто сдался первым.

  • Сдался nginx: прошло fastcgi_read_timeout (по умолчанию 60 с) или proxy_read_timeout — клиент получает 504.
  • Сдался php-fpm: сработал request_terminate_timeout, мастер-процесс убил воркер, соединение оборвалось — nginx пишет upstream prematurely closed connection и отдаёт 502.
  • Сдался supervisor приложения: pm2 перезапустил Node по max_memory_restart (или systemd убил процесс по MemoryMax= и поднял его по Restart=), порт на секунду опустел — 502 с Connection refused.

Отсюда правило: таймаут в nginx должен быть чуть больше, чем в бэкенде, иначе вы будете видеть 502 там, где ожидаете 504, и искать не ту причину. Например, request_terminate_timeout = 60s и fastcgi_read_timeout 65s. И не поднимайте таймауты до 300 секунд «чтобы не падало»: это не лечит медленный запрос, а лишь занимает воркер дольше, приближая тот самый max_children.

Для приложений за proxy_pass при перезапуске помогает второй экземпляр и переключение:

upstream app {
    server 127.0.0.1:3000 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:3001 backup;
    keepalive 16;
}
server {
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_next_upstream error timeout http_502;
    }
}

Без proxy_http_version 1.1 и пустого Connection директива keepalive не работает: nginx по умолчанию ходит к upstream по HTTP/1.0 и закрывает соединение после каждого запроса. При деплое сначала перезапускается один экземпляр, потом второй; nginx на время недоступности первого уходит на backup, и 502 почти исчезают — останутся только запросы, которые в момент рестарта уже были отправлены на умирающий экземпляр.

Как узнать о 502 раньше клиентов

Особенность 502 в том, что сервер при этом выглядит здоровым: nginx отвечает, порт 443 открыт, ping идёт, uptime растёт. Проверка «пингуется ли сервер» её не заметит; заметит только HTTP-запрос к реальной странице с проверкой кода ответа — и лучше с нескольких сетей, чтобы отличить 502 у всех от проблемы в одном канале. Разово это можно проверить с точек в России и Европе без регистрации, постоянно — мониторингом.

Что стоит настроить:

  1. Внешняя HTTP-проверка главной и одной «тяжёлой» страницы (корзина, поиск, личный кабинет), ожидаемый код 200–299, интервал 1–2 минуты. Тяжёлая страница первой упрётся в max_children и покажет 502, когда главная ещё отдаётся из кеша. Такой мониторинг доступности сайта с подтверждением сбоя с нескольких точек как раз отделяет реальный 502 от сетевого сбоя между одной точкой и хостингом.
  2. Уведомление туда, куда вы смотрите: сообщение в Telegram с причиной вида «HTTP 502 вместо 200–299» приходит сразу после подтверждения сбоя, и вы начинаете с tail error.log, а не с разбора жалоб.
  3. Проверка ключевой фразы на странице в дополнение к коду: заглушка хостинга, страница «сайт на обслуживании» или пустой шаблон при слетевшем виртуальном хосте нередко приходят с кодом 200 и обманывают проверку только по коду. Если сайт за Cloudflare, добавьте IP точек мониторинга в белый список (их выдают по запросу — см. страницу о роботе), чтобы страница защиты не считалась сбоем.
  4. Локально — алерт на строку reached pm.max_children в логе php-fpm и на Killed process в журнале ядра: это предвестники 502, которые появляются за часы до жалоб.

Коротко

  • 502 отдаёт прокси (nginx, Apache, Cloudflare, балансировщик), когда бэкенд отверг соединение, оборвал его или ответил невалидно. Чинить нужно бэкенд и его связку с прокси.
  • 503 — сервер сам отказал (перегрузка, maintenance), 504 — бэкенд отвечал слишком долго или соединение не установилось за таймаут. За Cloudflare отказ и таймаут соединения с origin — это 521 и 522, а не 502.
  • Сначала curl -I и заголовок Server, чтобы понять слой; затем error.log — строка с upstream называет причину почти всегда.
  • Самые частые причины: остановленный php-fpm после обновления PHP, неверный путь к сокету, исчерпанный pm.max_children, OOM-killer, слишком большие заголовки для fastcgi_buffer_size.
  • pm.max_children считается от свободной памяти и размера воркера; таймаут nginx делайте чуть больше таймаута бэкенда.
  • Сервер при 502 выглядит живым, поэтому нужен внешний HTTP-мониторинг с проверкой кода и уведомлением, а не ping.

Вопросы и ответы

Ошибка 502 Bad Gateway — это проблема на моей стороне или на стороне сайта?
На стороне сайта. 502 отдаёт сервер-посредник (nginx, Cloudflare, балансировщик), когда не смог получить корректный ответ от приложения. Очистка кеша или смена браузера не помогут; можно только подождать или сообщить владельцу сайта.
Почему 502 появляется на несколько секунд и сам проходит?
Чаще всего бэкенд перезапускается: деплой, автоперезапуск по памяти, воркер php-fpm убит OOM-killer. На время перезапуска порт или сокет пуст, nginx получает отказ в соединении и отдаёт 502. Лечится вторым экземпляром приложения и proxy_next_upstream.
502 Bad Gateway после обновления PHP — что случилось?
После обновления старый пакет php8.x-fpm удалён или его служба остановлена, а в fastcgi_pass у nginx по-прежнему указан старый сокет /run/php/php8.x-fpm.sock. В error.log будет строка «No such file or directory» или «Connection refused». Переключите fastcgi_pass на актуальный сокет и убедитесь, что нужный юнит запущен и включён в автозагрузку.
Поможет ли перезагрузка сервера от ошибки 502?
Временно — если причина в упавшем процессе или исчерпанной памяти. Но без исправления причины (число воркеров, путь к сокету, утечка памяти) ошибка вернётся. Сначала посмотрите error.log и journalctl, потом перезапускайте только нужную службу, а не весь сервер.
Что означает 502 в Cloudflare, если сервер работает?
Cloudflare соединился с origin, но получил от него невалидный или оборванный HTTP-ответ — либо origin сам отдал 502 (тогда в теле будет страница nginx, а не Cloudflare). Если бы фаервол блокировал IP Cloudflare или порт был закрыт, вы увидели бы 521 или 522. Проверьте origin напрямую по IP через curl с параметром --resolve, минуя Cloudflare.
Сколько ставить pm.max_children в php-fpm?
Столько, сколько помещается в свободную память: измерьте средний RSS одного воркера (обычно 30–80 МБ), вычтите из памяти сервера то, что занимают база и nginx, и разделите остаток на размер воркера с запасом 15–20 %. Значения по умолчанию 5 хватает только для сайта без нагрузки.

Не проверяйте вручную — следите автоматически

Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.

Подключить бесплатно Проверить сайт сейчас