504가 뜻하는 것
프록시(nginx, 로드밸런서, CDN)가 요청을 뒤쪽 서버로 넘기고 응답을 기다리다가, 자기 타이머가 끝나자 포기한 것입니다. RFC 9110의 표현으로는 upstream 서버로부터 “제때(timely) 응답을 받지 못한” 경우입니다. 502와의 차이가 여기 있습니다. 504는 깨진 것이 돌아온 게 아니라, 적어도 제시간에는 아무것도 돌아오지 않은 것입니다.
여기서 두 가지가 따라 나옵니다.
첫째, 504는 타이머에 관한 이야기입니다. 어떤 계층에 한도가 있고(흔히 30초, 60초, 100초), 요청이 그보다 오래 살았습니다. 오류가 나기까지 걸린 시간이 가장 좋은 단서입니다. 어느 계층의 타이머가 끝났는지 알려 주기 때문입니다.
둘째, 백엔드가 실패한 게 아닐 수 있습니다. 아직 일하고 있을 수도 있고, 끝까지 마칠 수도 있습니다. 프록시가 기다리기를 그만뒀을 뿐, DB 쿼리나 리포트 내보내기나 결제 호출이 취소됐다는 뜻은 아닙니다.
방문자라면
내 기기가 원인이 아니고, 브라우저를 바꿔도 소용없습니다. 몇 분 뒤에 다시 시도하세요. 사이트가 과부하일 때 504는 몰려서 나는 경우가 많습니다.
무언가를 제출한 직후(주문, 결제, 메일을 보내는 양식) 504가 떴다면 바로 다시 제출하지 마세요. 프록시가 나를 포기한 뒤에도 서버는 그 작업을 끝냈을 수 있습니다. 먼저 메일이나 계정에서 확인 내역을 찾아보세요.
회사 네트워크에서만 504가 나고 오류 화면에 회사 프록시 이름이 찍혀 있다면, 타임아웃이 난 것은 우리 회사 프록시입니다. 휴대폰 데이터로 접속해 보세요.
걸린 시간 재기
curl로 상태 코드와 걸린 시간을 함께 볼 수 있습니다.
curl -s -o /dev/null -w 'code=%{http_code} connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
https://example.com/reports/annual
code=504 connect=0.014s ttfb=60.042s total=60.043s
연결은 빠르고 첫 바이트까지의 시간이 딱 떨어지는 숫자에 붙어 있다면 전형적인 모습입니다. 이 숫자를 내 구성에 있는 계층의 기본값과 맞춰 보세요.
| 계층 | 기본 한도 | 발동했을 때 보이는 것 |
|---|---|---|
nginx proxy_read_timeout (fastcgi_read_timeout, uwsgi_read_timeout도 같음) | 60초 | 504, 제목이 “504 Gateway Time-out”이고 nginx 꼬리말이 붙은 페이지 |
nginx proxy_connect_timeout | 60초 | 504, 로그에 “while connecting to upstream” |
| AWS Application Load Balancer idle timeout | 60초 | 504, Server: awselb/2.0 |
| Amazon CloudFront origin response timeout | 30초 | 504, “Generated by cloudfront (CloudFront)” 페이지 |
| Cloudflare 프록시 | 100초 | 504가 아니라 Cloudflare 524 에러 |
| Gunicorn 워커 타임아웃 | 30초 | 504가 아니라 502: gunicorn이 워커를 죽이고 nginx는 연결이 끊긴 것만 봄 |
이것은 기본값입니다. 내 숫자가 어느 것과도 맞지 않는다면 누군가 다른 값을 설정한 것입니다. nginx가 실제로 쓰는 값은 nginx -T | grep -n timeout으로 볼 수 있습니다.
마지막 두 줄이 중요합니다. Cloudflare 뒤에서는 원 서버 타임아웃이 524로 나오므로, Cloudflare 뒤에서 보이는 평범한 504는 그 뒤쪽 계층, 대개 우리 nginx나 로드밸런서가 만든 것입니다. 그리고 앱 서버의 타이머가 프록시보다 짧으면, 실제로는 느린 요청인 것이 502로 보입니다. 가장 짧은 타이머가 사용자가 보는 코드를 정합니다.
HTTP 점검은 바깥 방문자가 받는 상태 코드와 Server 헤더를 보는 데 편하지만, 15초가 지나면 기다리기를 멈춥니다. 그래서 1분을 채워야 타임아웃이 나는 요청은 그곳에서 ‘응답 없음’으로 보입니다. 오래 걸리는 요청은 내 컴퓨터에서 curl로 재세요.
어느 계층이 만들었는지
헤더와 페이지 본문을 보세요.
curl -sv -o /dev/null https://example.com/reports/annual 2>&1 | grep -iE '^< (HTTP|server|via|x-cache|cf-ray)'
Server: awselb/2.0에 아무 장식 없는 페이지: ALB가 대상을 기다리다 타임아웃.X-Cache: Error from cloudfront: CloudFront가 원 서버를 기다리다 타임아웃.- nginx 꼬리말이나
Server: nginx: nginx가 upstream을 기다리다 타임아웃. - HAProxy의 기본 504 페이지에는 “The server didn’t respond in time.”이라고 나옵니다.
- Cloudflare 뒤에서는 모든 응답에
Server: cloudflare가 붙으므로 본문을 보세요. 평범한 nginx 504 페이지가 그대로 전달됐다면 원 서버의 nginx가 타임아웃을 낸 것입니다.
원인은 언제나 불평한 계층의 뒤에 있습니다. ALB가 504를 보냈다면 ALB가 아니라 ALB의 대상이 무엇을 하고 있었는지 봐야 합니다.
nginx 오류 로그 읽기
nginx가 504를 만들면 이유를 로그에 남깁니다. 대략 이런 줄입니다.
2026/10/07 14:03:51 [error] 2114#2114: *9310 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 198.51.100.7, server: example.com, request: "GET /reports/annual HTTP/1.1", upstream: "http://127.0.0.1:8000/reports/annual", host: "example.com"
“while” 뒤의 문구가 어느 단계에서 시간이 끝났는지 알려 줍니다.
- “while reading response header from upstream”: nginx는 연결해서 요청을 보냈는데, 백엔드가
proxy_read_timeout(PHP라면fastcgi_read_timeout) 안에 응답을 시작하지 않았습니다. 백엔드가 느린 것이고, 가장 흔한 경우입니다. - “while connecting to upstream”: nginx가
proxy_connect_timeout안에 TCP 연결조차 끝내지 못했습니다. 무언가가 패킷을 버리고 있거나(거부하지 않고 조용히 버리는 방화벽이나 보안 그룹), 주소가 틀렸거나, 백엔드의 accept 큐가 가득 찼습니다. 연결이 거부됐다면 즉시 502가 났을 것입니다. 여기서의 타임아웃은 침묵을 뜻합니다. - “while reading upstream”: 백엔드가 응답을 시작했다가 본문 도중에 read timeout보다 오래 멈췄습니다.
proxy_read_timeout에는 고치는 방법을 바꾸는 세부 사항이 하나 있습니다. 이것은 응답 전체에 대한 한도가 아니라, 연속된 두 번의 읽기 사이 최대 간격입니다. 20초마다 한 바이트씩 보내는 백엔드는 60초 read timeout에 절대 걸리지 않습니다. server-sent events나 long-polling 엔드포인트가 heartbeat가 있으면 살아남고 없으면 죽는 이유입니다.
흔한 원인
1. 특정 경로가 정말로 느림
504가 엔드포인트 하나를 따라다닙니다. 리포트, 내보내기, 이상한 필터를 건 검색, 모든 것을 불러오는 관리자 화면. 정적 파일과 홈페이지는 멀쩡합니다.
시간이 어디서 쓰이는지 찾으세요. 프록시를 빼고 보려면 프록시 서버에서 백엔드를 직접 호출합니다.
time curl -s -o /dev/null http://127.0.0.1:8000/reports/annual
그다음 흔한 용의자를 봅니다. 인덱스 없는 쿼리(DB의 slow query log에 나옵니다), 작은 쿼리 수천 개를 도는 N+1 루프, 그 자체로 느리거나 죽어 있는 외부 API를 동기로 호출하는 경우. PHP라면, 리눅스에서 max_execution_time은 DB나 네트워크를 기다리는 시간을 세지 않는다는 점을 기억하세요. 느린 쿼리에 막힌 스크립트는 그 한도를 넘겨서도 계속 살아 있고, 결국 nginx의 60초가 먼저 끝납니다.
진짜 해결은 그 경로를 빠르게 만들거나, 작업을 요청 밖으로 빼는 것입니다. 정당하게 몇 분이 걸리는 일이라면 작업을 접수하고 작업 ID와 함께 202 Accepted를 돌려준 뒤, 큐에서 처리하고 페이지가 진행 상태를 조회하게 하세요. 이 방식은 어떤 프록시의 타임아웃과도 상관이 없습니다.
2. 백엔드가 포화돼서 전부 느림
504가 몰려서 나고, 여러 경로에 걸치며, 트래픽 피크와 겹칩니다. 요청 하나하나는 빠른데, 빈 워커를 기다리며 줄을 서 있는 것입니다.
단서는 앱 서버 로그에 있습니다. 예를 들어 PHP-FPM은 대략 이런 경고를 남깁니다.
WARNING: [pool www] server reached pm.max_children setting (10), consider raising it
그 한도를 넘는 요청은 소켓의 backlog에서 기다리다가 nginx가 포기합니다. 비슷한 한도는 어디에나 있습니다. gunicorn 워커 수, 바닥난 DB 커넥션 풀, Java 앱 서버의 스레드 풀. 풀을 키우는 건 서버에 메모리가 있고 DB가 그만큼의 연결을 받아 줄 때만 도움이 됩니다. 실제로는 느린 요청 몇 개(원인 1)가 워커를 전부 붙잡고, 나머지가 그 뒤에 줄 서 있는 경우가 많습니다.
3. 낡은 upstream 주소
nginx는 proxy_pass의 호스트명을 시작하거나 리로드할 때 한 번만 해석하고 그 IP를 계속 씁니다. upstream이 IP가 바뀌는 대상이라면(클라우드 로드밸런서, 재배치된 컨테이너, 관리형 서비스) nginx는 계속 옛 주소로 연결합니다. 그 주소가 이제 패킷을 조용히 버린다면 “while connecting to upstream” 타임아웃이 나고, 리로드하면 마법처럼 고쳐집니다.
지금 그 이름이 무엇으로 해석되는지 dig +short internal-api.example.com으로(공개 이름이라면 DNS 조회로) 확인하고, 로그 줄의 upstream: 주소와 비교하세요. 해결은 nginx가 다시 해석하게 만드는 것입니다. resolver를 정의하고 proxy_pass에 변수를 쓰세요.
resolver 10.0.0.2 valid=30s;
set $api_upstream http://internal-api.example.com;
location /api/ {
proxy_pass $api_upstream;
}
proxy_pass에 변수가 들어 있으면 nginx는 요청 시점에 resolver로 이름을 해석합니다. (변수를 쓰면 nginx가 요청 URI를 넘기는 방식이 고정 URI를 쓸 때와 달라질 수 있으니 경로를 테스트하세요.)
4. 거부하지 않고 버리는 방화벽
DNS 변경이 없는데 connect 타임아웃이 난다면, 대개 방화벽 규칙이나 보안 그룹이 프록시의 패킷을 조용히 버리고 있는 것입니다. 프록시 서버에서 확인하세요.
nc -vz -w 5 10.0.3.17 8000
거부는 즉시 돌아오고, 버림은 타임아웃까지 멈춰 있습니다. 백엔드가 원래 공개적으로 접근 가능해야 하는 곳이라면 포트 점검이 같은 것을 바깥에서 확인해 줍니다. 내부 주소는 프록시 서버에서 한 테스트만 의미가 있습니다.
5. 체인 안의 타임아웃이 서로 맞지 않음
요청은 여러 계층을 지나고, 계층마다 시계가 있습니다. CDN, 로드밸런서, nginx, 앱 서버, DB. 가장 짧은 것이 먼저 발동합니다. nginx proxy_read_timeout을 300초로 올려도 ALB idle timeout이 60초 그대로라면, 사용자는 여전히 60초에 504를 받습니다. 이번에는 ALB에서 올 뿐입니다.
어떤 경로가 정말로 더 오래 걸려야 한다면 그 경로에서만, 바깥 계층부터 차례로 올리세요.
location /reports/ {
proxy_pass http://app;
proxy_read_timeout 300s;
}
그다음 로드밸런서의 idle timeout, 그다음 앱 서버 자체의 워커 타임아웃입니다. Cloudflare 뒤라면 대부분의 요금제에서 기본 100초가 천장이므로, 그보다 느린 것은 어차피 백그라운드 작업이 돼야 합니다.
504를 없애려고 전역 타임아웃을 올리지는 마세요. 모든 곳의 타임아웃이 300초라면, 멈춘 백엔드가 이제 연결 하나하나를 1분이 아니라 5분씩 붙잡고, 작은 지연이 훨씬 빨리 전면 장애로 번집니다.
고친 다음에
curl -w명령을 다시 돌려 보세요. 건강한 경로는 타임아웃 바로 아래가 아니라 한참 아래에서 돌아옵니다. 60초 한도에 58초 걸리는 응답은 바쁜 날을 기다리고 있는 504입니다.- 그 경로를 불러오면서
sudo tail -f /var/log/nginx/error.log를 지켜보세요. “upstream timed out” 줄이 더 생기지 않아야 합니다. - 작업을 백그라운드로 옮겼다면 진행 상태 조회 경로도, 작업이 실패했을 때의 동작까지 포함해 테스트하세요.
비슷해 보이는 다른 오류
- 502 Bad Gateway: 프록시가 깨진 응답을 받았거나 연결이 실패한 것으로, 대개 즉시 돌아옵니다.
- 503 Service Unavailable: 서비스가 의도적으로 거부한 것입니다. 점검, rate limit, 정상 백엔드 없음.
- Cloudflare 522 에러: Cloudflare가 원 서버와 TCP 연결 자체를 끝내지 못했습니다.
- ERR_CONNECTION_TIMED_OUT: 브라우저가 사이트의 앞단 서버에 닿지도 못해서 HTTP 상태 코드가 아예 없습니다.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.