502가 실제로 알려 주는 것
502는 내가 접속하려는 애플리케이션이 만드는 오류가 아닙니다. 그 앞에 있는 무언가, 즉 nginx, 로드밸런서, CDN, ingress controller가 만듭니다. RFC 9110은 502를 게이트웨이나 프록시가 요청을 처리하려고 접근한 서버로부터 “유효하지 않은 응답(invalid response)을 받은” 경우로 정의합니다.
실제로 ‘유효하지 않은 응답’은 형식이 깨진 HTTP보다 범위가 넓습니다. 연결 자체를 열지 못했을 때(거부됨, 소켓 없음), 백엔드가 응답 도중 연결을 끊었을 때, 백엔드의 헤더가 프록시 버퍼에 들어가지 않을 때도 프록시는 502를 냅니다. 공통점은 이렇습니다. 프록시는 살아 있고, 시도도 했는데, 뒤에서 돌아온 것이 쓸 수 없는 것이었습니다.
이웃 코드와는 이렇게 구분됩니다.
| 코드 | 프록시가 겪은 일 | 보통 걸리는 시간 |
|---|---|---|
| 502 | 깨진 응답을 받았거나 연결이 바로 실패함 | 대개 즉시 |
| 504 | 연결했거나 연결을 시도했고, 기다리다 타이머가 끝남 | 30초, 60초, 100초처럼 딱 떨어지는 시간 |
| 503 | 아예 전달하지 않기로 함: 점검, rate limit, 정상 백엔드 없음 | 즉시 |
걸린 시간은 공짜로 얻는 첫 단서입니다. 20밀리초 만에 돌아오는 502는 연결 거부나 프로세스 다운입니다. 정확히 30초 걸린 ‘502’라면 한 번 더 봐야 합니다. 어떤 앱 서버는 느린 요청을 스스로 죽이는데, 그러면 프록시는 연결이 끊긴 것만 보고 실제로는 타임아웃인 것을 502로 보고합니다(아래에서 다룹니다).
“505 Bad Gateway”로 검색하셨다면, bad gateway 코드는 502입니다. 505는 HTTP Version Not Supported라는 전혀 다른, 드문 오류입니다.
방문자라면
내 기기에서 고칠 것은 없습니다. 브라우저는 사이트의 프록시까지 잘 도착했고, 실패한 곳은 그 프록시와 뒤쪽 서버 사이입니다. 1분쯤 뒤에 새로고침하세요. 배포 중에 나는 502는 대개 몇 초 안에 사라지고, 몇 시간째 계속된다면 운영자만 해결할 수 있는 장애입니다.
예외가 하나 있습니다. 회사 네트워크에서만 오류가 나고 오류 화면에 회사 프록시 이름이 찍혀 있다면, 502를 낸 것은 우리 회사 프록시입니다. 휴대폰 데이터로 접속해 보면 확인할 수 있습니다.
어느 계층이 보냈는지 찾기
운영 환경에는 대개 프록시가 두세 개 줄지어 있고, 그중 어느 것이든 502를 낼 수 있습니다. 로드밸런서가 불평하는데 nginx를 고치고 있으면 오후가 통째로 날아갑니다. 응답 헤더와 오류 페이지 본문을 보세요.
curl -sv -o /dev/null https://example.com/ 2>&1 | grep -iE '^< (HTTP|server|via|x-cache|cf-ray|x-amz-cf-id)'
흔한 계층별 모양은 이렇습니다.
- nginx:
Server: nginx(버전이 붙기도 함). 기본 페이지는 “502 Bad Gateway” 제목 하나와 “nginx” 꼬리말 한 줄입니다. - AWS Application Load Balancer:
Server: awselb/2.0, 꼬리말 없는 최소한의 HTML 페이지. - Amazon CloudFront:
X-Cache: Error from cloudfront, 그리고 “502 ERROR … The request could not be satisfied”, “Generated by cloudfront (CloudFront)“라는 문구가 있는 페이지. - HAProxy: 기본 페이지에 “The server returned an invalid or incomplete response.”라고 나옵니다.
- Apache mod_proxy: “Proxy Error” 페이지에 “The proxy server received an invalid response from an upstream server.”
- Cloudflare: 프록시를 거치는 모든 응답에
Server: cloudflare와CF-Ray헤더가 붙으므로, 헤더만으로는 누가 오류를 만들었는지 알 수 없습니다. 본문을 보세요. Cloudflare 브랜드가 찍힌 페이지면 Cloudflare가 만든 것이고, nginx 꼬리말이 붙은 평범한 페이지면 원 서버의 502가 그대로 전달된 것입니다. Cloudflare와 원 서버 사이의 문제는 보통 522나 525 같은 Cloudflare 고유의 52x 코드로 나옵니다.
HTTP 점검은 우리 네트워크 밖에서 본 최종 상태 코드와 Server를 포함한 응답 헤더를 보여 줍니다. 내 컴퓨터에서는 재현이 안 될 때 쓸모 있습니다.
어느 계층이 만들었는지 알면, 원인은 언제나 그 계층의 바로 뒤에 있습니다. ALB 흔적이 있는 502라면 ALB의 대상(흔히 우리 nginx)이 잘못된 응답을 준 것이고, nginx 흔적이 있는 502라면 nginx의 upstream(앱, PHP-FPM, 컨테이너)이 그런 것입니다.
nginx 오류 로그 읽기
nginx가 만든 502라면 오류 로그 한 줄에 이유가 적혀 있습니다. 대부분의 배포판에서는 이렇게 봅니다.
sudo tail -n 50 /var/log/nginx/error.log
# 컨테이너라면
docker logs --tail 50 <nginx-container>
502마다 [error] 항목이 하나씩 남습니다. errno 뒤의 문구가 핵심입니다. 흔한 경우의 모양과 뜻은 다음과 같습니다.
Connection refused: 듣고 있는 프로세스가 없음
대략 이런 줄입니다.
2026/10/07 09:12:44 [error] 2114#2114: *8812 connect() failed (111: Connection refused) while connecting to upstream, client: 198.51.100.7, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:3000/", host: "example.com"
앱이 떠 있지 않거나, 죽었거나, proxy_pass에 적힌 것과 다른 포트나 주소에서 듣고 있습니다. 프록시 서버에서 직접 확인하세요.
sudo ss -ltnp | grep 3000
curl -v http://127.0.0.1:3000/
systemctl status myapp
journalctl -u myapp --since "15 min ago"
프로세스가 계속 죽는다면 커널의 OOM killer를 의심하세요. 조용히 프로세스를 죽이는 대표적인 원인입니다. journalctl -k | grep -i "killed process"로 확인합니다.
자주 걸리는 변형이 세 가지 있습니다.
localhost가 IPv6로 해석됨.proxy_pass http://localhost:3000이라고 쓰면 nginx는localhost를127.0.0.1과::1둘 다로 해석해 번갈아 씁니다. 앱이 IPv4에서만 듣고 있으면 요청의 절반가량이 로그에upstream: "http://[::1]:3000/"을 남기며 실패합니다.127.0.0.1로 명시하세요.- Docker 네트워크. nginx 컨테이너 안에서
127.0.0.1은 앱이 아니라 nginx 컨테이너 자신입니다. 같은 네트워크에 있는 앱의 서비스명이나 컨테이너명을 쓰고, 앱이 자기 컨테이너 안에서127.0.0.1이 아니라0.0.0.0에 바인딩하는지 확인하세요. - 배포 중의 공백. 재시작할 때마다 몇 초 동안은 아무것도 듣고 있지 않습니다. 502가 배포 시각에 몰려 있다면 graceful reload를 쓰거나, health check 뒤에서 rolling deploy를 해서 준비된 인스턴스로만 트래픽이 가게 하세요.
소켓이 없거나 권한이 없음: PHP-FPM
대략 이런 줄입니다.
connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory) while connecting to upstream
connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied) while connecting to upstream
“No such file”은 PHP-FPM이 떠 있지 않거나 소켓이 다른 곳에 있다는 뜻입니다. PHP를 업그레이드한 직후에 자주 보입니다. 새 버전의 소켓 이름은 바뀌었는데(php8.3-fpm.sock) fastcgi_pass는 여전히 옛 이름을 가리키는 경우입니다. pool 설정(예: /etc/php/8.2/fpm/pool.d/www.conf)의 listen = 줄과 nginx -T에 나오는 fastcgi_pass를 비교하세요.
“Permission denied”는 소켓은 있는데 nginx 워커 사용자가 열 수 없다는 뜻입니다. pool의 listen.owner, listen.group, listen.mode를 nginx 사용자(www-data나 nginx)가 접근할 수 있게 설정하고 PHP-FPM을 재시작하세요.
RHEL 계열에서 127.0.0.1:8080 같은 TCP upstream에 (13: Permission denied)가 난다면, 대개 파일 권한이 아니라 SELinux가 nginx의 네트워크 연결을 막는 것입니다. 표준 해결은 sudo setsebool -P httpd_can_network_connect 1이고, ausearch -m avc -ts recent로 거부 기록을 볼 수 있습니다.
upstream이 연결을 닫거나 리셋함
대략 이런 줄입니다.
upstream prematurely closed connection while reading response header from upstream
recv() failed (104: Connection reset by peer) while reading response header from upstream
nginx는 연결해서 요청을 보냈는데, 백엔드가 응답을 끝내기 전에 끊었습니다. 흔한 이유는 이렇습니다.
- 이 요청에서 앱이 죽음. segfault, 워커를 죽이는 처리되지 않은 예외, 메모리 부족으로 인한 강제 종료. 같은 시각의 애플리케이션 로그에 대개 흔적이 있습니다.
- 앱 서버가 느린 워커를 죽임. Gunicorn의 워커 타임아웃 기본값은 30초입니다. 이게 발동하면 gunicorn은
[CRITICAL] WORKER TIMEOUT을 남기고 워커를 죽이며, nginx는 502를 보고합니다. PHP-FPM의request_terminate_timeout도 같은 일을 합니다. 이것은 변장한 타임아웃입니다. 고칠 대상은 느린 요청(또는 타임아웃 값)이고, 504 Gateway Timeout과 같은 방법으로 접근합니다. 어떤 코드가 보일지는 어느 타이머가 더 짧은지에 달렸습니다. 앱 서버가 먼저 죽이면 502, nginx의proxy_read_timeout이 먼저 끝나면 504입니다. - keep-alive 경합. 프록시가 놀고 있던 연결을 재사용하는 바로 그 순간, 백엔드가 그 연결을 닫기로 하는 경우입니다. 대표적인 예가 AWS ALB 뒤의 Node.js입니다. Node의
keepAliveTimeout기본값은 5초, ALB의 idle timeout 기본값은 60초라서 일부 요청이 간헐적으로 502를 받습니다. 원칙은 백엔드의 keep-alive 타임아웃이 프록시보다 길어야 한다는 것입니다. Node라면server.keepAliveTimeout을 ALB idle timeout보다 길게(예: 65초),server.headersTimeout을 그보다 조금 더 길게 잡으세요.
헤더가 버퍼보다 큼
대략 이런 줄입니다.
upstream sent too big header while reading response header from upstream
백엔드의 응답 헤더가 nginx의 헤더 버퍼에 들어가지 않는 경우입니다. 기본값은 메모리 페이지 하나(플랫폼에 따라 4KB 또는 8KB)입니다. 세션이나 분석 도구 쿠키 때문에 커진 Set-Cookie 헤더가 가장 흔한 원인이고, 큰 인증 헤더나 CSP 헤더가 그다음입니다. 일부 사용자(큰 쿠키를 가진 사람)에게만, 또는 로그인한 뒤에만 나는 경향이 있습니다.
버퍼 크기와 버퍼 개수 설정을 함께 올리세요. 하나만 올리면 nginx -t가 busy buffer 크기 오류를 냅니다.
# proxy_pass
proxy_buffer_size 16k;
proxy_buffers 8 16k;
# PHP-FPM / fastcgi_pass
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
그다음 sudo nginx -t && sudo systemctl reload nginx. 그리고 헤더가 왜 이렇게 커졌는지도 물어보세요. 계속 자라는 쿠키는 어떤 버퍼든 결국 넘어섭니다.
No live upstreams: 살아 있는 upstream이 없음
대략 이런 줄입니다.
no live upstreams while connecting to upstream, client: 198.51.100.7, server: example.com, request: "GET / HTTP/1.1", upstream: "http://app_backend/", host: "example.com"
이것은 원인이 아니라 결과입니다. upstream 블록의 모든 서버가 최근에 실패해서(fail_timeout 안에 max_fails 도달) nginx가 시도를 멈추고, 연결하지 않은 채 502를 돌려주는 것입니다. 로그를 위로 올려 서버들을 다운으로 표시하게 만든 이전 오류를 찾으세요. 그게 진짜 이유입니다.
upstream과의 TLS 실패
proxy_pass가 https://라면 SSL_do_handshake() failed … while SSL handshaking to upstream 같은 줄을 찾으세요. 백엔드가 그 포트에서 TLS를 하지 않거나, SNI가 필요한 경우입니다. nginx는 기본적으로 upstream에 SNI를 보내지 않으므로 proxy_ssl_server_name on;을 추가하세요(upstream 이름이 다르면 proxy_ssl_name도).
로드밸런서가 502를 낸 경우
Server 헤더가 awselb/2.0이라면 ALB의 대상이 잘못된 응답을 준 것입니다. AWS는 ALB 502의 이유로 대상이 연결을 닫았거나 형식이 잘못된 응답을 보낸 경우, 대상 그룹이 HTTPS일 때 대상과의 TLS 핸드셰이크가 잘못된 경우 등을 문서에 적어 두었습니다. 실무에서는 위의 keep-alive 불일치가 단연 흔합니다. ALB 액세스 로그는 elb_status_code와 target_status_code를 따로 기록합니다. 대상 상태가 -라면 대상이 유효한 응답을 아예 만들지 못한 것입니다.
Kubernetes ingress-nginx는 로그 형식이 nginx와 같으므로 위의 메시지가 그대로 적용됩니다. 재시작 중이거나 crashloop에 빠진 Pod는 Connection refused를 내고, 아무 Pod와도 맞지 않는 Service selector는 대개 502가 아니라 503을 냅니다.
바깥에서 확인하기
서버 안에서 502가 재현되지 않을 때는 바깥에서 두 가지를 확인하면 범위가 좁혀집니다.
- HTTP 점검으로 일반 방문자가 받는 상태 코드와 헤더, 그리고 502가 매번 나는지 간헐적인지를 봅니다.
- DNS 조회로 호스트명이 생각하는 곳을 가리키는지 확인합니다. 이전 작업 뒤에 옛 A 레코드가 남아 있으면 트래픽 일부가 뒤에 아무것도 없는 폐기된 서버의 프록시로 갑니다.
포트 점검은 upstream이 공개적으로 접근 가능한 경우(예: CDN 뒤의 원 서버 IP)에만 도움이 됩니다. 127.0.0.1:3000 같은 내부 upstream은 위처럼 프록시 서버에서 curl과 ss로 확인해야 합니다.
고친 다음에
- 실패하던 URL을 여러 번 새로고침하세요. 간헐적 502(keep-alive 경합, 여러 대 중 한 대만 불량)는 한 번의 요청으로는 배제할 수 없습니다.
- 그동안 오류 로그를 지켜보세요.
sudo tail -f /var/log/nginx/error.log. 고쳐졌다면 새 줄이 더 생기지 않습니다. - 502가 배포 시각과 겹쳤다면 로그를 보면서 배포를 한 번 해 보세요. 무중단 배포가 정말 되는지 확인하는 유일한 방법입니다.
비슷해 보이는 다른 오류
- 504 Gateway Timeout: 프록시가 연결은 했지만 응답을 너무 오래 기다렸습니다.
- 503 Service Unavailable: 서비스가 의도적으로 거부하고 있거나, 과부하이거나, 정상 백엔드가 없습니다.
- Cloudflare 522 에러: Cloudflare가 원 서버와 TCP 연결을 열지 못했습니다.
- ERR_CONNECTION_REFUSED: 같은 ‘듣고 있는 프로세스 없음’ 문제를 프록시 없이 브라우저가 직접 본 경우입니다.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.