- Published on
OpenAI Responses API 502 Bad Gateway 원인과 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡해 보이는데도 502 Bad Gateway가 튀어나오면 디버깅이 급격히 어려워집니다. 특히 OpenAI Responses API를 호출하는 구조가 클라이언트 → (내 서비스) → (프록시/ALB/Cloudflare/Nginx) → OpenAI처럼 다단계일 때, 502는 “어딘가의 게이트웨이가 업스트림으로부터 정상 응답을 못 받았다”는 뭉뚱그린 신호일 뿐입니다.
이 글은 502를 원인별로 쪼개서 재현 가능하게 만들고, 코드 레벨/인프라 레벨에서 재시도·폴백·서킷브레이커·타임아웃·스트리밍 설정으로 실제 운영 장애를 줄이는 방법을 다룹니다.
502의 정체를 먼저 분해하자
502 Bad Gateway는 보통 다음 중 하나입니다.
- 중간 프록시/로드밸런서가 업스트림(OpenAI 또는 내 백엔드)과 연결/응답 처리에 실패
- 스트리밍(SSE) 응답이 프록시에서 버퍼링/타임아웃/압축 설정 문제로 깨짐
- 클라이언트/서버의 타임아웃 불일치로 연결이 끊기고, 프록시가 502로 변환
- 순간적인 OpenAI 측 장애(또는 엣지)로 업스트림 응답이 비정상
핵심은 “OpenAI가 502를 줬다”가 아니라, 누가 502를 최종적으로 반환했는지를 먼저 확인하는 것입니다.
누가 502를 반환했는지 확인하는 체크
- 응답 헤더에
server: cloudflare,via,x-cache,x-amzn-trace-id같은 흔적이 있나? - 내 서비스가 Nginx/ALB 뒤에 있다면, ALB/Nginx 액세스 로그에 502가 찍히는지
- OpenAI 호출은 성공했는데 내 서버→클라이언트 구간에서 502가 나는지
가능하면 OpenAI 호출 구간에 다음을 남기세요.
request_id(OpenAI가 주는 식별자) 또는 응답 헤더의 추적 정보- 업스트림 호출 시작/종료 시각, 소요시간
- 스트리밍이면 첫 토큰까지 시간(TTFT), 총 토큰 시간
대표 원인 1: 프록시/로드밸런서 타임아웃이 OpenAI 응답보다 짧다
가장 흔한 실무 패턴입니다.
- OpenAI 응답이 35초 걸릴 수 있는데
- ALB idle timeout이 30초
- Nginx
proxy_read_timeout이 30초
이러면 업스트림이 정상이어도 중간에서 연결을 끊고 502/504로 바꿔서 내려보냅니다.
해결 전략
- 업스트림(OpenAI) 타임아웃과 프록시 타임아웃을 일관되게 맞춥니다.
- 스트리밍을 쓴다면 주기적으로 데이터가 흘러가게(flush) 설정합니다.
Nginx 예시(스트리밍/SSE 포함)
location /api/llm {
proxy_http_version 1.1;
proxy_set_header Connection "";
# 업스트림이 늦게 말해도 끊지 않기
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# SSE/스트리밍이면 버퍼링이 502/끊김의 원인이 되기 쉬움
proxy_buffering off;
# gzip이 SSE를 망가뜨리는 경우가 있어 구간별로 끄는 걸 권장
gzip off;
}
타임아웃을 더 체계적으로 다루려면 408/타임아웃 재현부터 잡는 게 빠릅니다. 상황에 따라 502로 보이지만 본질은 타임아웃인 경우가 많습니다. 관련해서는 OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드도 같이 보세요.
대표 원인 2: SSE 스트리밍이 프록시에서 버퍼링/압축/idle로 끊긴다
Responses API를 스트리밍으로 붙여서 UI에 토큰을 흘릴 때, 502는 종종 OpenAI가 아니라 내 프록시가 SSE를 제대로 전달 못해서 발생합니다.
증상 예:
- 브라우저
EventSource failed또는 fetch 스트림이 중간에 종료 - 서버 로그에는 OpenAI 스트림 수신이 계속되는데, 클라이언트는 끊김
- Cloudflare/Nginx/ALB 뒤에서만 재현
해결 체크리스트
- Nginx:
proxy_buffering off,gzip off - Cloudflare: 버퍼링/캐싱/압축 정책 점검
- ALB: idle timeout 상향
- 애플리케이션: 주기적으로 flush (SSE라면
\n\n단위로 이벤트 전송)
프록시 뒤 스트리밍 이슈는 케이스가 많아서 별도 체크리스트가 훨씬 효율적입니다. 아래 글의 설정 항목을 그대로 대입해 보세요.
대표 원인 3: 순간 장애(업스트림 5xx)인데 재시도 전략이 없다
OpenAI든 네트워크든, 순간적인 5xx는 현실적으로 피할 수 없습니다. 문제는 한 번의 502가 곧바로 사용자 오류로 전파되는 구조입니다.
여기서 중요한 포인트:
- 502/503/500은 대개 재시도 가치가 있는 오류
- 단, 무작정 재시도하면 더 큰 장애(폭주)를 만든다
Best Practice: 지수 백오프 + 지터 + 서킷브레이커
- 재시도 횟수 제한(예: 2~4회)
- 지수 백오프(예: 0.5s, 1s, 2s, 4s) + 랜덤 지터
- 일정 비율로 실패하면 서킷 오픈 → 빠른 실패 + 폴백
이미 500/503 대응을 정리한 실전 패턴이 있다면 502에도 거의 그대로 적용됩니다.
Python 예시(httpx) - 502 재시도 래퍼
import random
import time
import httpx
RETRYABLE_STATUS = {500, 502, 503, 504}
def post_with_retry(url: str, headers: dict, payload: dict, timeout_s: float = 60.0):
max_attempts = 4
base = 0.5
with httpx.Client(timeout=httpx.Timeout(timeout_s)) as client:
for attempt in range(1, max_attempts + 1):
try:
r = client.post(url, headers=headers, json=payload)
if r.status_code in RETRYABLE_STATUS:
if attempt == max_attempts:
r.raise_for_status()
# exponential backoff + jitter
sleep_s = base * (2 ** (attempt - 1)) + random.uniform(0, 0.25)
time.sleep(sleep_s)
continue
r.raise_for_status()
return r.json()
except (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError) as e:
if attempt == max_attempts:
raise
sleep_s = base * (2 ** (attempt - 1)) + random.uniform(0, 0.25)
time.sleep(sleep_s)
# 사용 예
# url = "https://api.openai.com/v1/responses"
# headers = {"Authorization": f"Bearer {OPENAI_API_KEY}"}
# payload = {"model": "gpt-4.1-mini", "input": "hello"}
# data = post_with_retry(url, headers, payload)
운영에서는 “재시도 자체”보다 재시도 트래픽이 폭주를 만들지 않게 큐잉/버짓/동시성 제한을 같이 넣는 게 중요합니다.
대표 원인 4: 내 서버(게이트웨이)가 먼저 죽는다 (worker timeout, 메모리, 커넥션)
502가 OpenAI 때문이라고 생각하기 쉬운데, 실제로는 내 API 서버가 업스트림 호출 중에 타임아웃/재시작/worker kill로 죽어서 프록시가 502를 반환하는 경우가 많습니다.
자주 나오는 패턴
- Gunicorn/Uvicorn worker timeout으로 프로세스 강제 종료
- 동시 요청 증가로 이벤트 루프가 막힘(특히 동기 I/O 혼용)
- 커넥션 풀 고갈/파일 디스크립터 고갈
Gunicorn timeout 예시 점검
--timeout이 OpenAI 호출 최대 시간보다 짧지 않은가?- 스트리밍 응답인데 worker가 “응답이 없다”고 판단하고 죽이지 않는가?
이 케이스는 아래 글이 재현부터 해결까지 가장 빠릅니다.
트러블슈팅: 30분 안에 원인 좁히는 실전 절차
1) 같은 요청을 “직접 OpenAI”로 호출해서 재현되는지 확인
- 내 백엔드/프록시를 우회해서 로컬에서 직접 호출
- 동일 입력/동일 모델로 502가 재현되면 업스트림/네트워크 가능성이 커짐
2) 스트리밍/비스트리밍을 바꿔서 비교
- 스트리밍에서만 502면 프록시 버퍼링/압축/idle 가능성이 큼
- 비스트리밍에서도 502면 타임아웃/순간 장애/서버 자원 문제 가능성
3) 타임아웃 “삼각관계”를 표로 맞춘다
- 클라이언트 타임아웃
- 내 서버(ASGI) 타임아웃
- Nginx/ALB/Cloudflare 타임아웃
- OpenAI 호출 타임아웃
가장 짧은 타임아웃이 전체를 지배합니다.
4) 관측성 최소 세트
- 요청 단위 trace id
- 업스트림 호출 시간, 재시도 횟수, 최종 상태코드
- 스트리밍이면 TTFT, 총 스트림 시간
운영 Best Practice: 502를 “사용자 장애”로 만들지 않는 설계
1) 재시도는 서버에서, 사용자에게는 일관된 오류 계약
- 클라이언트에서 무작정 재시도하면 중복 요청/중복 과금/UX 붕괴가 발생
- 서버에서 idempotency(가능하면)와 함께 재시도
2) 폴백 모델/폴백 응답
- 502가 연속으로 나면 더 작은 모델로 폴백하거나
- “요약 품질 저하”를 감수하고 캐시된 답/부분 답을 반환
3) 서킷브레이커로 장애 전파 차단
- 특정 구간에서 502가 폭증하면 일정 시간 빠른 실패
- 그동안 큐잉/지연/재시도로 시스템 전체가 무너지는 걸 방지
4) 스트리밍은 프록시 친화적으로
- 버퍼링/압축/idle timeout을 스트리밍에 맞게 조정
- 가능하면
/stream엔드포인트는 별도 location으로 설정 분리
결론
OpenAI Responses API에서 502를 만났을 때 중요한 건 “OpenAI가 나쁨”이 아니라, 게이트웨이/프록시/타임아웃/스트리밍/내 서버 리소스 중 어디가 실제 병목인지 빠르게 분리하는 것입니다.
오늘 바로 할 일은 3가지입니다.
- 502를 반환한 주체를 로그/헤더로 식별하고, 타임아웃 체인을 표로 정리하기
- 502/503/500에 대해 지수 백오프+지터 재시도와 서킷브레이커를 적용하기
- 스트리밍을 쓴다면 Nginx/ALB/Cloudflare의 버퍼링·gzip·idle timeout을 스트리밍 친화적으로 바꾸기
이 3가지만 적용해도 “가끔 나는 502”가 “사용자가 체감하는 장애”로 번지는 비율이 확 줄어듭니다.