Published on

OpenAI Responses API 502 Bad Gateway 원인과 해결

Authors

서버가 멀쩡해 보이는데도 502 Bad Gateway가 튀어나오면 디버깅이 급격히 어려워집니다. 특히 OpenAI Responses API를 호출하는 구조가 클라이언트 → (내 서비스) → (프록시/ALB/Cloudflare/Nginx) → OpenAI처럼 다단계일 때, 502는 “어딘가의 게이트웨이가 업스트림으로부터 정상 응답을 못 받았다”는 뭉뚱그린 신호일 뿐입니다.

이 글은 502를 원인별로 쪼개서 재현 가능하게 만들고, 코드 레벨/인프라 레벨에서 재시도·폴백·서킷브레이커·타임아웃·스트리밍 설정으로 실제 운영 장애를 줄이는 방법을 다룹니다.

502의 정체를 먼저 분해하자

502 Bad Gateway는 보통 다음 중 하나입니다.

  1. 중간 프록시/로드밸런서가 업스트림(OpenAI 또는 내 백엔드)과 연결/응답 처리에 실패
  2. 스트리밍(SSE) 응답이 프록시에서 버퍼링/타임아웃/압축 설정 문제로 깨짐
  3. 클라이언트/서버의 타임아웃 불일치로 연결이 끊기고, 프록시가 502로 변환
  4. 순간적인 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가지입니다.

  1. 502를 반환한 주체를 로그/헤더로 식별하고, 타임아웃 체인을 표로 정리하기
  2. 502/503/500에 대해 지수 백오프+지터 재시도와 서킷브레이커를 적용하기
  3. 스트리밍을 쓴다면 Nginx/ALB/Cloudflare의 버퍼링·gzip·idle timeout을 스트리밍 친화적으로 바꾸기

이 3가지만 적용해도 “가끔 나는 502”가 “사용자가 체감하는 장애”로 번지는 비율이 확 줄어듭니다.