Ошибка 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
| Кто отдал | Как выглядит | Что это значит |
|---|---|---|
| nginx | Server: 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 |
| Cloudflare | Server: cloudflare, заголовок cf-ray, страница с Ray ID | origin принял соединение, но ответил не-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_timeout | dmesg, лог 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 у всех от проблемы в одном канале. Разово это можно проверить с точек в России и Европе без регистрации, постоянно — мониторингом.
Что стоит настроить:
- Внешняя HTTP-проверка главной и одной «тяжёлой» страницы (корзина, поиск, личный кабинет), ожидаемый код 200–299, интервал 1–2 минуты. Тяжёлая страница первой упрётся в
max_childrenи покажет 502, когда главная ещё отдаётся из кеша. Такой мониторинг доступности сайта с подтверждением сбоя с нескольких точек как раз отделяет реальный 502 от сетевого сбоя между одной точкой и хостингом. - Уведомление туда, куда вы смотрите: сообщение в Telegram с причиной вида «HTTP 502 вместо 200–299» приходит сразу после подтверждения сбоя, и вы начинаете с
tail error.log, а не с разбора жалоб. - Проверка ключевой фразы на странице в дополнение к коду: заглушка хостинга, страница «сайт на обслуживании» или пустой шаблон при слетевшем виртуальном хосте нередко приходят с кодом 200 и обманывают проверку только по коду. Если сайт за Cloudflare, добавьте IP точек мониторинга в белый список (их выдают по запросу — см. страницу о роботе), чтобы страница защиты не считалась сбоем.
- Локально — алерт на строку
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 появляется на несколько секунд и сам проходит?
502 Bad Gateway после обновления PHP — что случилось?
Поможет ли перезагрузка сервера от ошибки 502?
Что означает 502 в Cloudflare, если сервер работает?
Сколько ставить pm.max_children в php-fpm?
Не проверяйте вручную — следите автоматически
Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.
Подключить бесплатно Проверить сайт сейчас