Published on

OpenAI Responses API 413 에러 업로드 용량 제한과 청크 전략

Authors

서론

프로덕션에서 멀쩡히 돌던 멀티모달 기능이 갑자기 413 Payload Too Large로 터지면, 대부분 팀은 “OpenAI가 불안정한가?”부터 의심합니다. 하지만 413은 대개 서버가 요청 바디(또는 업로드된 파일)를 너무 크다고 판단해 거절했다는 뜻입니다. 문제는 이 에러가 단순히 “파일이 큼”에서 끝나지 않는다는 점입니다.

  • 이미지가 base64로 들어가면서 원본보다 33% 이상 부풀어 실제 페이로드가 제한을 넘는다
  • 프록시/로드밸런서(Nginx/ALB/Cloudflare)가 더 작은 제한으로 먼저 막는다
  • RAG 파이프라인이 PDF/HTML을 한 번에 올리며 텍스트+메타데이터+툴 호출 결과까지 합쳐져 폭발한다

이 글에서는 Responses API에서 413이 발생하는 대표 케이스를 분해하고, 이미지·파일 업로드 용량 제한을 예측 가능하게 관리하는 방법과 청크(Chunk) 전략을 코드 중심으로 정리합니다.


413이 의미하는 것: “모델”이 아니라 “요청/업로드”가 거절됨

HTTP 413은 애플리케이션 로직 이전 단계에서 발생하는 경우가 많습니다.

  1. 클라이언트 → 프록시(Nginx/Envoy/Cloudflare): client_max_body_size 같은 설정에 걸려 413
  2. 프록시 → OpenAI API 게이트웨이: OpenAI 측 업로드/요청 바디 제한에 걸려 413
  3. SDK에서 base64 인코딩/멀티파트 구성: 실제 전송 바이트가 예상보다 커짐

즉, 413은 “프롬프트가 길다”보다 **“전송 바이트가 크다”**에 가깝습니다. 프롬프트 토큰 제한과는 다른 축입니다.


어디서 커졌는지 먼저 측정하기: 바이트 단위 계측

JSON 바디 크기 측정(전송 전)

Python에서 요청 JSON을 만들기 전에 UTF-8 바이트 길이를 재면, base64 포함 여부에 따른 폭증을 바로 확인할 수 있습니다.

import json

def payload_size_bytes(payload: dict) -> int:
    return len(json.dumps(payload, ensure_ascii=False).encode("utf-8"))

payload = {
    "model": "gpt-4.1-mini",
    "input": "...",
}

print("bytes:", payload_size_bytes(payload))

base64는 생각보다 위험하다

  • base64는 원본 대비 약 4/3(≈1.33배) 증가
  • JSON 문자열 이스케이프/헤더/경계 문자열까지 더해져 추가 증가

따라서 “10MB 이미지니까 괜찮겠지”가 아니라, 전송 페이로드는 13~15MB까지도 튈 수 있습니다.


이미지 업로드에서 413을 줄이는 실전 전략

1) base64 inline 대신 URL/파일 업로드 경로를 분리

가능하면 이미지를 외부 스토리지(S3, GCS, 사내 CDN)에 올리고 URL로 참조하세요. inline base64는 디버깅은 쉽지만, 413의 가장 흔한 트리거입니다.

  • 장점: 요청 바디가 매우 작아짐
  • 단점: URL 접근 제어/서명 URL/만료 관리 필요

2) 리사이즈/압축을 “서버 표준”으로 고정

멀티모달에서 모델이 필요한 정보는 대개 “원본 4K”가 아닙니다. 다음을 정책으로 고정하면 413이 급감합니다.

  • 최대 변 길이: 1024~1536px
  • 포맷: JPEG/WebP(텍스트 위주면 PNG 유지)
  • 품질: 70~85

Pillow로 리사이즈 + JPEG 압축

from PIL import Image
import io

def compress_image_to_jpeg_bytes(path: str, max_side=1536, quality=80) -> bytes:
    img = Image.open(path)
    img = img.convert("RGB")

    w, h = img.size
    scale = min(max_side / max(w, h), 1.0)
    if scale < 1.0:
        img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)

    buf = io.BytesIO()
    img.save(buf, format="JPEG", quality=quality, optimize=True)
    return buf.getvalue()

jpeg_bytes = compress_image_to_jpeg_bytes("input.png")
print("compressed bytes:", len(jpeg_bytes))

이렇게 만든 바이너리를 업로드하거나(멀티파트), base64로 넣더라도 폭증이 훨씬 줄어듭니다.

3) 여러 장 이미지는 “한 요청”이 아니라 “배치 + 합성”으로

한 번에 10장을 올리면, 각 이미지가 작아도 합산으로 413이 납니다.

  • (권장) 2~3장씩 나눠 요청하고 결과를 합성
  • (대안) 썸네일/타일링으로 1장으로 합쳐서 업로드(단, 정보 손실 주의)

파일 업로드에서 413을 줄이는 청크 전략

핵심 원칙: “원문 전체”를 올리는 대신, 업로드 파이프라인을 나눠라

PDF/HTML/로그 덤프를 통째로 모델에 보내는 방식은 초기에만 쉬워 보이고, 운영에서는 413/비용/지연이 함께 터집니다.

대신 다음 3단계로 분리하세요.

  1. 추출(Extract): PDF → 텍스트, HTML → 본문 텍스트
  2. 정규화(Normalize): 공백/중복 제거, 표/코드 블록 처리
  3. 청킹(Chunking): 제한 바이트/토큰 예산에 맞춰 분할

바이트 기반 청킹(안전한 하한선)

토큰 기반이 이상적이지만, 413은 바이트가 직접 원인이므로 먼저 바이트로 안전하게 자르는 게 효과적입니다.

def chunk_by_bytes(text: str, max_bytes: int = 200_000):
    # UTF-8 기준으로 max_bytes 이하로 분할
    chunks = []
    buf = []
    size = 0

    for line in text.splitlines(keepends=True):
        b = len(line.encode("utf-8"))
        if size + b > max_bytes and buf:
            chunks.append("".join(buf))
            buf, size = [], 0
        buf.append(line)
        size += b

    if buf:
        chunks.append("".join(buf))
    return chunks

text = open("dump.txt", "r", encoding="utf-8").read()
chunks = chunk_by_bytes(text, max_bytes=120_000)
print(len(chunks), "chunks")
  • max_bytes프록시 제한/헤더/JSON 오버헤드를 감안해 넉넉히 잡습니다.
  • 줄 단위로 자르면 문장 중간 절단을 어느 정도 피할 수 있습니다.

토큰 예산까지 고려한 청킹(품질 최적화)

RAG/요약 작업에서는 토큰 예산을 함께 봐야 답변 품질이 안정됩니다. 청킹 전략이 품질에 미치는 영향은 아래 글의 디버깅 체크리스트가 특히 도움이 됩니다.

실무 팁:

  • 오버랩(겹침): 5~15% 정도로 문맥 손실을 줄임
  • 구조 기반 분할: 마크다운 헤더/HTML 섹션/로그 타임스탬프 기준
  • 중요도 기반 축약: “전체 전송” 대신 먼저 목차/요약을 만들고 필요한 섹션만 상세 질의

Responses API 호출 설계: 큰 입력은 “업로드 → 참조 → 처리”로

대용량 입력을 한 방에 넣기보다, 다음 패턴을 추천합니다.

  1. 업로드/저장(사내 스토리지나 파일 API)
  2. 모델에는 식별자/URL/요약 메타만 전달
  3. 필요한 경우에만 특정 청크를 추가로 요청

이 패턴은 413뿐 아니라 비용/지연도 같이 줄여줍니다.

만약 요청 형식이 잘못되어 400이 섞여 나온다면, 413과 구분해서 먼저 입력 스키마를 점검하세요.


트러블슈팅 체크리스트: “OpenAI 제한”이 아니라 “우리 프록시”일 수도

1) Nginx/Envoy/Cloudflare의 업로드 제한 확인

  • Nginx: client_max_body_size
  • Envoy: max_request_bytes
  • Cloudflare: 플랜별 업로드 제한/프록시 제한
  • ALB/API Gateway: 바디 크기 제한

증상:

  • OpenAI 응답 바디가 아니라 프록시 HTML 에러 페이지가 온다
  • 서버 로그에 upstream까지 가지 않고 413이 난다

LLM 스트리밍을 함께 쓰는 경우, 프록시 튜닝 이슈가 413과 동반되기도 합니다(버퍼링/타임아웃/최대 바디 제한이 같이 꼬임).

2) 로깅: “원본 파일 크기”가 아니라 “전송 페이로드 크기”를 남겨라

운영에서 필요한 로그 필드:

  • 원본 파일 바이트
  • 리사이즈/압축 후 바이트
  • base64 적용 후 예상 바이트(×1.33)
  • 최종 JSON 바디 바이트
  • 프록시를 포함한 응답 헤더(가능하면)

이렇게 남기면 “어느 단계에서 커졌는지”가 바로 보입니다.

3) 재시도는 만능이 아니다: 413은 재시도하면 대부분 또 실패

413은 일시 장애가 아니라 결정적 거절인 경우가 많습니다. 재시도 정책은 다음처럼 분기하세요.

  • 429/500/503: 지수 백오프 재시도
  • 413: 즉시 입력 축소/압축/청킹으로 폴백

재시도/폴백 설계를 제대로 잡아두면 장애 전파가 줄어듭니다.


Best Practice: 413을 “설계로 예방”하는 운영 패턴

1) 입력 게이트웨이에서 사전 검증

API 서버에 업로드가 들어오는 순간 다음을 수행하세요.

  • MIME 타입/확장자 화이트리스트
  • 최대 바이트 제한(원본)
  • 이미지면 리사이즈/압축 후 바이트 제한
  • 텍스트면 정규화 후 청킹

즉, 모델 호출 직전에야 크기를 확인하면 이미 늦습니다.

2) “큰 입력”은 비동기 잡으로 넘겨라

대용량 문서 처리(추출→청킹→요약)는 요청-응답 동기 흐름에 넣지 말고:

  • 작업 큐(Celery/RQ/SQS)
  • 결과 폴링 또는 웹훅

으로 분리하면, 413뿐 아니라 타임아웃/메모리 스파이크도 같이 줄어듭니다.

3) 사용자 경험 설계: 업로드 실패 대신 자동 경량화

사용자에게 “너무 큽니다”만 던지지 말고:

  • “자동으로 1536px로 줄여서 다시 시도”
  • “문서를 20개 섹션으로 나눠 처리 중, 완료되면 알려드림”

같은 폴백 UX를 제공하면 이탈이 크게 줄어듭니다.


결론

OpenAI Responses API 413은 대부분 모델 문제가 아니라 요청/업로드 바이트가 제한을 넘은 설계 문제입니다. 해결의 핵심은 세 가지입니다.

  1. 바이트 계측으로 어디서 페이로드가 커지는지 먼저 고립
  2. 이미지/파일은 압축·리사이즈 + URL/업로드 참조 + 배치 분할로 전송량 제어
  3. 텍스트/문서는 추출→정규화→청킹으로 “한 방 전송”을 없애기

지금 운영 중인 파이프라인에서 (a) base64 inline을 쓰고 있거나 (b) 문서를 통째로 보내고 있다면, 오늘 바로 리사이즈 정책 + 바이트 기반 청킹 + 413 폴백부터 넣어보세요. 413은 거의 항상 사라지고, 비용과 지연도 함께 내려갑니다.