nginx 502 Bad Gateway: Diagnostic Chart and Copy-Paste Commands to Fix It in 5 Minutes
Why a 502 When the Frontend Looks Fine?
An alert fires and the browser shows 502 Bad Gateway. The first thing your hands reach for is systemctl restart nginx—but pause. A 502 is far more likely not nginx's fault.
The critical difference between a 502 and a 4xx is that the request itself is valid. A 4xx is a client (request) problem; a 502 means nginx accepted the client's request and tried to pass it to an upstream (php-fpm, gunicorn, uvicorn, node, etc.) but got an invalid response from the upstream, or the connection itself dropped. nginx is just a healthy middleman—the real culprit is almost always the upstream.
That's why blindly restarting nginx is risky. If the cause is an upstream OOM or timeout, restarting nginx will never fix it—and you'll only wipe connection state and log context, making root-cause analysis harder. The sequence is clear: check error.log keywords → match the diagnostic chart → verify with copy-paste commands → fix the right place.
502 Diagnostic Chart (8 Common Cases)
First open /var/log/nginx/error.log, grab the keyword, and match it against the table below.
| Cause | error.log keyword | Check command | One-line fix |
|---|---|---|---|
| Upstream down | connect() failed (111: Connection refused) | sudo systemctl status php8.2-fpm | Start/restart the upstream process |
| Connect timeout | upstream timed out ... while connecting | curl -v http://127.0.0.1:8000/health | Check firewall/network and proxy_connect_timeout |
| Read timeout | upstream timed out ... while reading response header | journalctl -u gunicorn -n 50 | Align proxy_read_timeout with the app --timeout |
| Response buffer too small | upstream sent too big header | nginx -T | grep buffer | Increase proxy_buffer_size/fastcgi_buffer_size |
| Unix socket permissions | connect() ... Permission denied | ls -l /run/php/php-fpm.sock | Fix listen.owner/group/mode |
| SELinux blocking | connect() failed (13: Permission denied) | sudo getsebool httpd_can_network_connect | setsebool -P httpd_can_network_connect 1 |
| Health failure / no nodes | no live upstreams while connecting | ss -tlnp | grep 9000 | Restore upstream nodes; proxy_next_upstream |
| Premature connection close | upstream prematurely closed connection | journalctl -u uvicorn -n 100 | Check gateway timeout/OOM/keepalive |
Narrow the Cause by Reading error.log Keywords
If the table isn't enough, copy-paste the commands below in order to narrow it down. Each command notes what it checks.
# 1) 업스트림 프로세스가 살아있나? (Active: running 확인)
sudo systemctl status php8.2-fpm
# 2) 업스트림이 기대한 포트에서 실제로 리스닝 중인가?
ss -tlnp | grep 9000
# 3) nginx를 건너뛰고 업스트림을 직접 호출 — 여기서 200이면 nginx 설정 문제
curl -v http://127.0.0.1:8000/health
# 4) 유닉스 소켓 방식이면 소켓으로 직접 호출
curl --unix-socket /run/php/php-fpm.sock http://localhost/
# 5) nginx 설정 문법 검사 후 무중단 reload
sudo nginx -t && sudo systemctl reload nginx
# 6) gunicorn/uvicorn 게이트웨이 로그에서 OOM·timeout 흔적 찾기
journalctl -u gunicorn -n 50The key is step 3. If you skip nginx and hit the upstream directly and get a healthy response, the problem is in nginx config (port, socket, timeout). If the direct call also fails, the culprit is 100% the upstream. That one line cuts the blame in half.
Copy-Paste Fix Recipes by Cause
proxy_pass (gunicorn/uvicorn/node) Timeout and Buffer Tuning
# Before — 기본값이라 느린 응답·큰 헤더에서 502
location / {
proxy_pass http://127.0.0.1:8000;
}
# After — 타임아웃과 버퍼를 현실에 맞게
location / {
proxy_pass http://127.0.0.1:8000;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 32k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 64k;
}If you see upstream sent too big header, increasing proxy_buffer_size is the right fix. Large cookies or JWT headers overflowing the default buffer is common.
fastcgi_pass (php-fpm) Socket vs Port
# 유닉스 소켓 방식 — 같은 호스트면 빠르지만 권한 함정 주의
fastcgi_pass unix:/run/php/php-fpm.sock;
# TCP 포트 방식 — 컨테이너/원격 분리 시 권장
fastcgi_pass 127.0.0.1:9000;
# 공통: 버퍼/타임아웃도 fastcgi_ 접두어로 따로 설정
fastcgi_read_timeout 60s;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 16k;A mistyped socket path or port mismatch is a classic cause of connect() failed. Always compare the actual listening address from ss -tlnp | grep 9000 against your config.
SELinux and Socket Permission Traps
On RHEL/Rocky/AlmaLinux, SELinux defaults to enforcing, so nginx is blocked when it tries to open a network connection to the upstream. The symptom is connect() failed (13: Permission denied)—if the port is open but you get permission denied, suspect SELinux.
# 현재 정책 상태 확인 (off면 이게 범인)
sudo getsebool httpd_can_network_connect
# 영구 허용 (-P 옵션이 재부팅 후에도 유지)
sudo setsebool -P httpd_can_network_connect 1If you use a Unix socket, the php-fpm pool owner/group/permissions must match the nginx runtime user. /etc/php/8.2/fpm/pool.d/www.conf:
listen = /run/php/php-fpm.sock
listen.owner = www-data ; nginx 실행 유저 (RHEL 계열은 nginx)
listen.group = www-data
listen.mode = 0660Permission denied is common when sharing a socket via a volume between containers—if UIDs/GIDs differ across containers, you can't put them in the same group. In that case, TCP (127.0.0.1:9000) is simply less painful.
upstream prematurely closed connection — Tweaking nginx Alone Won't Fix It
This is the most confusing case. upstream prematurely closed connection while reading response header means the upstream closed the connection first while nginx was waiting for a response. That's why bumping nginx timeouts alone doesn't fix it. The real cause is on the gateway side.
- gunicorn/uvicorn's own timeout: the worker is killed because it hit gunicorn's default
--timeout 30 - Worker OOM: the worker dies from memory exhaustion and the connection drops (look for
Worker ... was sent SIGKILL!injournalctl -u gunicorn) - keepalive mismatch: nginx tries to reuse the connection but the upstream closes keepalive
A real-world case: we raised nginx proxy_read_timeout to 120s, but still got 502s around 60s. Turns out gunicorn --timeout was set to 60s, so gunicorn killed the worker first no matter how long nginx was willing to wait. Without the habit of looking at both timeouts together, you'll wander this trap for a long time.
Recurrence Prevention Checklist
upstream app {
server 127.0.0.1:8000;
keepalive 32; # 연결 재사용으로 부하·지연 감소
}
server {
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # keepalive 사용 시 필수
proxy_read_timeout 90s; # 아래 앱 timeout보다 살짝 크게
proxy_next_upstream error timeout http_502;
}
}- Align timeouts: if gunicorn
--timeout 60, set nginxproxy_read_timeouthigher (e.g. 90s) so the gateway doesn't die first. - Health-check endpoint: expose
/healthfor the load balancer/monitoring to poll, and useproxy_next_upstreamto skip dead nodes automatically. - Habit of graceful reload: apply config changes with
sudo nginx -t && sudo systemctl reload nginx. Use reload instead of restart for zero-downtime.
When 502 shows up again, just remember this sequence: check error.log keywords → curl the upstream directly → match the diagnostic chart → fix the one right place → reload. Recovery speed is a world apart from the days of restarting on a guess.
References: Official Docs
The primary source for the behavior, settings, and errors covered in this post is the official documentation below. Check there for version-specific options and exact behavior.
FAQ
Q. Restarting nginx doesn't clear the 502. Why?
A. 502 is usually an upstream (php-fpm/gunicorn/uvicorn) problem, so restarting nginx won't fix it. Hit the upstream directly with curl -v http://127.0.0.1:포트/health. If that fails too, the culprit is 100% the upstream.
Q. How do I catch upstream prematurely closed connection?
A. Don't blame the nginx timeout—suspect the gateway's own timeout (gunicorn --timeout), worker OOM, or a keepalive mismatch. In journalctl -u gunicorn -n 100, look for SIGKILL or timeout traces, then set the app timeout smaller than nginx proxy_read_timeout.
Q. The port is open but I get connect() failed (13: Permission denied).
A. SELinux on RHEL/Rocky is the likely blocker. Check with sudo getsebool httpd_can_network_connect, then allow it with sudo setsebool -P httpd_can_network_connect 1. If you use a Unix socket, listen.owner/group/mode must also match the nginx user.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.