Published on

OpenAI Responses API 502 Bad Gateway 원인과 해결

Authors
Binance registration banner

서버가 멀쩡해 보이는데도 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”가 “사용자가 체감하는 장애”로 번지는 비율이 확 줄어듭니다.