- Published on
AWS ALB 502·504 난사 - 원인별 해결 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡해 보이는데 ALB(Application Load Balancer) 앞에서만 502/504가 난사하는 상황은 생각보다 흔합니다. 문제는 "ALB가 죽었다"가 아니라, ALB가 백엔드(타깃)와 통신하는 과정에서 실패하거나, 정해진 시간 안에 응답을 못 받거나, 연결을 유지하는 정책이 맞지 않아서 발생하는 경우가 대부분이라는 점입니다.
이 글은 502/504를 한 덩어리로 보지 않고, 헬스체크(Health Check), Idle timeout, HTTP/2, 그리고 애플리케이션/네트워크 타임아웃으로 원인을 쪼개서 진단→재현→해결 순서로 정리합니다. 특히 “간헐적”, “트래픽 피크에만”, “특정 엔드포인트만” 같은 조건부 장애를 빠르게 잡는 데 초점을 둡니다.
> 스트리밍(SSE/웹소켓/긴 응답) 환경이라면 프록시/버퍼링/TTFB까지 함께 봐야 합니다. 관련 체크리스트는 LLM SSE 스트리밍 499 502 급증과 응답 끊김도 참고하세요.
1) 먼저 개념 정리: ALB의 502 vs 504
502 Bad Gateway (ALB 관점)
대체로 ALB ↔ 타깃 사이에서 다음이 발생했을 때 나옵니다.
- 타깃이 연결을 리셋(RST)하거나 조기 종료
- 타깃이 잘못된 HTTP 응답(헤더/프레이밍)을 반환
- TLS(HTTPS 타깃) 핸드셰이크 실패
- HTTP/2 프레이밍/헤더 규칙 위반(특히 gRPC/HTTP2 혼용 시)
즉, “백엔드가 죽었다”라기보다 ALB가 백엔드 응답을 정상 HTTP로 해석하지 못했거나 연결이 끊긴 경우가 많습니다.
504 Gateway Timeout (ALB 관점)
ALB가 타깃에 요청을 전달했지만 정해진 시간 내에 응답을 못 받았을 때 주로 나옵니다.
- 타깃(앱)이 느림: DB 락/외부 API 지연/GC stop-the-world
- 타깃 연결은 됐지만 응답 헤더가 늦음(TTFB 증가)
- ALB/중간 프록시/앱의 타임아웃 값 불일치
핵심은 504는 “느림”이고, 502는 “깨짐/끊김/규약 불일치”에 가깝다는 점입니다.
2) 진단 0순위: 로그/지표로 원인 범위를 좁히기
ALB Access Log에서 필드로 빠르게 판별
ALB 액세스 로그를 S3에 켜고, 다음 필드를 우선 봅니다.
elb_status_code(ALB가 클라이언트에 준 코드)target_status_code(타깃이 준 코드, 없으면-)target_processing_time(타깃 처리 시간)response_processing_time(ALB가 응답 처리한 시간)error_reason(가능하면 이 값이 결정적)
패턴 예시:
elb_status_code=504+target_status_code=-또는 타깃 시간이 길다 → 타깃 응답 지연/타임아웃elb_status_code=502+error_reason=Target.ResponseCodeMismatch등 → 타깃이 비정상 응답elb_status_code=502+target_status_code=-+ 매우 짧은 시간 → 연결 리셋/조기 종료
CloudWatch에서 함께 보는 지표
HTTPCode_ELB_5XX_Count(ALB 자체 5xx)HTTPCode_Target_5XX_Count(타깃 5xx)TargetResponseTime(p95/p99)HealthyHostCount(헬스체크 실패 여부)RejectedConnectionCount(스파이크 시 포화 신호)
여기서 ELB 5xx는 오르는데 Target 5xx는 안 오르는 패턴이면, 앱이 500을 내는 게 아니라 ALB↔타깃 경로에서 문제가 생겼을 확률이 큽니다.
3) 헬스체크 원인: “정상인데 비정상으로 판정”이 502/504를 만든다
헬스체크가 흔들리면 타깃이 등록/해제되며 다음 현상이 동반됩니다.
- 트래픽이 특정 인스턴스에 쏠려 지연 증가 → 504
- 연결 재사용이 깨지고 재시도 증가 → 502/504 혼재
- 배포/오토스케일 시 더 심해짐
자주 터지는 헬스체크 설정 실수
(1) 헬스체크 엔드포인트가 “무거움”
/health가 DB/Redis/외부 API까지 검사하면 피크에 가장 먼저 느려집니다. 헬스체크는 원칙적으로:
- 프로세스가 살아있고
- 이 인스턴스가 트래픽을 받을 준비가 됐는지
만 빠르게 확인해야 합니다.
권장: /live(liveness)와 /ready(readiness)를 분리하고, ALB에는 /ready를 사용하되 DB까지 강제하지 않도록 설계합니다.
(2) 성공 코드 범위가 너무 좁음
리다이렉트(301/302)나 204를 내는 헬스체크라면 ALB의 success codes에 포함해야 합니다.
- 기본값(200)만 두면 정상인데도 Unhealthy
(3) 타임아웃/인터벌이 현실과 불일치
- Timeout 2초, Interval 5초, Unhealthy threshold 2 같은 설정은 피크에 흔들리기 쉽습니다.
- 앱 워밍업이 필요한데 Healthy threshold가 낮으면 등록/해제가 반복됩니다.
예시: FastAPI에서 가벼운 readiness 구현
from fastapi import FastAPI
app = FastAPI()
@app.get("/ready")
def ready():
# DB ping 같은 무거운 작업은 피하고,
# 큐 컨슈머/필수 설정 로딩 여부 등만 확인
return {"status": "ok"}
@app.get("/live")
def live():
return "ok"
해결 체크리스트(헬스체크)
- 헬스체크 경로를 가볍게 만들었는가
- success codes에 실제 응답 코드가 포함되는가
- Timeout/Interval/Threshold가 트래픽 피크를 견딜 만큼 여유 있는가
- 배포 시 워밍업(캐시 로딩 등) 동안 Unhealthy로 빠지지 않게 했는가
4) Idle timeout 원인: “요청은 길고, 중간에 조용하면 끊긴다”
ALB에는 Idle timeout(기본 60초) 이 있습니다. 이 값은 “요청 전체 시간”이 아니라, 연결에서 데이터가 오가지 않는 유휴 시간에 가깝습니다.
문제가 되는 전형적인 상황:
- 서버가 2분짜리 작업을 하고 마지막에 한 번에 응답 → 중간이 조용해서 끊김(502/504/클라이언트 499 혼재)
- SSE/스트리밍에서 heartbeat 없이 오랫동안 이벤트가 안 나옴 → 중간에 끊김
- 업스트림(Nginx/Envoy)과 ALB의 keep-alive/idle 설정 불일치
해결 방향 1: ALB Idle timeout을 늘린다
긴 요청이 정상인 서비스(리포트 생성, 대용량 export, LLM 추론 등)라면 Idle timeout을 늘리는 게 1차 처방입니다.
다만 무작정 늘리면 느린 요청이 더 오래 자원을 잡아먹을 수 있으니, 애플리케이션 타임아웃/큐잉/비동기 작업 전환과 함께 고려해야 합니다.
해결 방향 2: 스트리밍은 heartbeat를 넣는다
SSE라면 주기적으로 :\n\n 같은 코멘트 프레임(heartbeat)을 보내 유휴를 깨야 합니다.
import time
from fastapi import FastAPI
from starlette.responses import StreamingResponse
app = FastAPI()
@app.get("/events")
def sse():
def gen():
while True:
# 15~30초마다 heartbeat
yield ": keep-alive\n\n"
time.sleep(15)
return StreamingResponse(gen(), media_type="text/event-stream")
스트리밍/프록시 조합에서 끊김이 잦다면 FastAPI Uvicorn에서 SSE 웹소켓 LLM 스트리밍이 프록시 뒤에서 끊길 때도 함께 보면 원인 분해가 빨라집니다.
해결 체크리스트(Idle timeout)
- 긴 작업이면 Idle timeout을 늘렸는가
- 스트리밍이면 heartbeat/flush가 주기적으로 발생하는가
- ALB 앞단(Cloudflare/Nginx)과 뒷단(앱 서버)의 timeout이 서로 모순되지 않는가
5) HTTP/2 원인: “클라이언트↔ALB는 H2, ALB↔타깃은 H1”의 함정
ALB는 클라이언트와는 HTTP/2를 지원하지만, 타깃 그룹으로는 기본적으로 HTTP/1.1을 사용합니다(일부 시나리오에서 gRPC/HTTP2 타깃 그룹을 별도로 구성).
여기서 502가 나는 흔한 케이스는:
- 앱/프록시가
Connection헤더, hop-by-hop 헤더를 잘못 다룸 - 프록시가 HTTP/2 요청을 HTTP/1.1로 변환하는 과정에서 헤더 크기/형식 문제가 발생
- gRPC를 HTTP 대상 그룹으로 붙이거나(반대로) 프로토콜을 혼용
증상별 힌트
- 특정 User-Agent(브라우저)에서만 발생: 브라우저는 H2를 적극 사용
- 특정 경로(대형 헤더/쿠키)에서만 502: 헤더 크기/압축 관련 가능성
- ALB 로그에
Target.ResponseCodeMismatch류가 보임: 타깃 응답이 HTTP 규약을 위반했을 가능성
해결 체크리스트(HTTP/2)
- gRPC면 gRPC 타깃 그룹을 사용했는가
- hop-by-hop 헤더(
Connection,Keep-Alive,Transfer-Encoding등)를 앱이 잘못 내려보내지 않는가 - 쿠키/헤더가 과도하게 커지지 않았는가(특히 인증 토큰을 쿠키에 누적)
6) “타임아웃 체인” 정렬: ALB-프록시-앱-업스트림의 값이 뒤죽박죽이면 504가 난다
504는 대부분 타임아웃 값의 체인 정렬로 해결됩니다. 원칙은 간단합니다.
- 가장 바깥(클라이언트/ CDN) 타임아웃이 가장 길고
- 그 안쪽(ALB)도 충분히 길고
- 앱 서버/업스트림(DB, 외부 API)의 타임아웃은 그보다 짧게
즉, 내부에서 먼저 실패하고(명확한 에러), 바깥에서는 그 에러를 전달하도록 만드는 게 운영에 유리합니다.
예:
- 외부 API 호출 타임아웃 10초
- 앱 전체 요청 타임아웃 30초
- ALB idle timeout 60~120초(스트리밍/긴 요청이면 더)
Python httpx에서 업스트림 타임아웃/재시도 예시
외부 API 지연이 504의 근본 원인인 경우가 많습니다. 앱에서 호출 타임아웃과 재시도를 명시하면, “ALB 504” 대신 “앱이 502/504를 제어된 방식으로 처리”할 수 있습니다.
import httpx
timeout = httpx.Timeout(connect=2.0, read=10.0, write=5.0, pool=2.0)
def fetch(url: str) -> dict:
with httpx.Client(timeout=timeout) as client:
r = client.get(url)
r.raise_for_status()
return r.json()
재시도/백오프까지 포함한 설계는 Python httpx ReadTimeout·ConnectError 재시도 설계도 같이 참고하면 좋습니다.
7) 재현으로 끝내기: 502/504를 “의도적으로” 만들어보면 해결이 빨라진다
현상만 보면 감으로 튜닝하게 되는데, 아래처럼 재현하면 원인 분리가 빨라집니다.
(1) 504 재현: 타깃에서 응답 지연
- 특정 엔드포인트에서
sleep(90)같은 지연을 넣고 - ALB idle timeout이 60초라면 504/502가 재현되는지 확인
FastAPI 예:
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/slow")
def slow():
time.sleep(90)
return {"ok": True}
(2) 502 재현: 조기 종료/연결 리셋
- 서버가 응답을 쓰다 프로세스가 죽거나
- reverse proxy가 upstream을 강제 종료하는 상황을 만들면
- ALB는 502를 내기 쉽습니다.
운영에서는 OOMKilled/프로세스 크래시/worker 재시작이 이런 패턴을 만듭니다. (컨테이너라면 메모리/CPU 제한과 함께 확인)
8) 운영에서 가장 흔한 “원인별 처방” 요약
A. 헬스체크가 흔들린다 (HealthyHostCount가 출렁)
- 헬스체크 엔드포인트를 가볍게
- success codes/timeout/threshold 재조정
- 배포/오토스케일 워밍업 반영
B. 피크에만 504가 난다 (TargetResponseTime p99 급등)
- 앱/DB 병목(락, 커넥션 풀 고갈) 확인
- 업스트림 타임아웃/재시도 설계
- 비동기 작업(큐)로 전환 고려
C. 특정 브라우저/특정 경로에서만 502
- HTTP/2 관련 헤더/프레이밍/쿠키 크기 점검
- gRPC/HTTP 대상 그룹 혼용 여부 확인
D. 스트리밍/긴 응답에서 끊긴다
- ALB idle timeout 증설
- SSE heartbeat/flush
- 중간 프록시 버퍼링/압축(gzip) 설정 점검
9) 마무리: “ALB 502/504”는 결과일 뿐, 원인은 대개 3가지다
ALB에서 502/504가 난사할 때는 결론적으로 다음 3축으로 수렴합니다.
- 헬스체크가 틀려서 정상 타깃을 빼거나 불안정하게 만든다
- Idle timeout/keep-alive가 맞지 않아 조용한 구간에서 연결이 끊긴다
- HTTP/2·프록시·타임아웃 체인 불일치로 ALB↔타깃 통신이 깨지거나 늦어진다
로그(특히 ALB access log의 error_reason)와 지표(HealthyHostCount, TargetResponseTime, ELB 5xx vs Target 5xx)를 먼저 보고, 그 다음에 헬스체크/Idle timeout/HTTP2를 순서대로 배제하면 “감”이 아니라 “증거”로 해결할 수 있습니다.