About

Azure OpenAI Batch 가 어느 날 전부 500 — 입력 파일 500개 한도와 그 400 을 덮은 중간 API

지우기 · 만료 걸기 · 주기 청소 중 무엇을 고를까, 그리고 4xx 를 뭉개지 않는 법

결론부터. Azure OpenAI Batch 를 돌리는 코드에 expires_after 가 없고 파일을 지우는 코드도 없다면, 그 기능은 언젠가 전부 멈춘다. 이 글을 읽으면 언제 멈출지 계산할 수 있고, 어떤 정리 방식을 고를지 정할 수 있다.

회사(Rails 로 만든 계약관리 SaaS)에서 계약서 항목을 AI 로 일괄 추출하는 기능이 어느 날 아침부터 모든 조직에서 첫 청크부터 실패했다. 고칠 때까지 일주일 가까이 걸렸다. Rails 쪽 에러 모니터링에 찍힌 건 이 한 줄뿐이었다.

Batch create failed (500)

500 이니까 우리 추출 서버가 죽었겠거니 했다. 틀렸다.

진짜 원인은 맨 아래 층에 있었다

Batch API 는 요청을 파일 하나에 모아 올리면 24시간 안에 비동기로 처리해 주는 API 다. 구성은 Rails Job → 추출 API(Python) → Azure OpenAI 고, 계약을 일정 건수씩 나눈 덩어리(청크)마다 배치를 하나 만든다. 그때마다 JSONL(한 줄에 요청 하나인 텍스트 파일) 입력 파일을 Files API 로 올리고, 끝나면 출력 파일을 읽는다. 그런데 아무도 파일을 안 지웠다.

Azure 는 만료를 안 건 배치 입력 파일을 리소스당 500개까지만 받는다. 한도를 넘자 Azure 는 400 quotaExceeded 를 돌려줬고, 추출 API 는 그 예외를 잡아 500 으로 바꿔 Rails 에 넘겼다.

The error loses its cause on the way back upAzure OpenAI400quotaExceededthe real cause is hereExtraction APIexcept Exception500status and message droppedRailsBatch createfailed (500)looks like our server brokeInput files without an expiry: 500 per resource. Nobody deleted them.The counter belongs to the whole resource, so every organization fails at once.

원인을 보려면 추출 API 쪽 에러까지 열어 봐야 했다. 호출한 쪽에는 400 인지 quota 인지 남아 있지 않았으니까.

「특정 조직 데이터 문제」 처럼 보였던 이유

첫 실패는 수천 건짜리 실행이었는데, 절반쯤 처리하다 멈췄다. 파일 수가 한도에 닿은 순간 남은 청크가 거절됐기 때문이다. 이것만 보면 그 조직의 데이터 어딘가가 이상해 보인다.

실제로는 리소스 전체가 공유하는 카운터 문제였다. 그 뒤로 어느 조직이 눌러도 실패했고, 사용자들은 몇 번씩 다시 눌렀다. 파일 목록을 세 보니 입력 파일이 501개였다. 문서의 500 과 한 개 차이가 왜 나는지는 모른다.

내 리소스는 언제 막히나

계산은 단순하다. 입력 파일은 배치당 하나니까 500 ÷ 한 달 배치 수 개월이다. 우리는 출시하고 두 달이 안 돼서 막혔다.

한 달 배치 수   막히는 시점
  50            10개월 뒤
 100             5개월 뒤
 300            약 1.7개월 뒤

청크로 나눠 보내면 사용자 실행 1번에 파일이 여러 개 생긴다. 실행 수가 아니라 청크 수로 세야 한다. 이미 쌓인 파일 수는 Files API 목록에서 purpose 가 batch 인 것을 세면 나온다.

지우기 vs 만료 걸기 vs 주기 청소

방식 막아 주는 것 약한 곳 언제
업로드 때 expires_after 한도가 10,000개로 오르고 14〜30일 뒤 사라진다 그동안 파일이 남는다. 출력은 따로 걸어야 한다 기본값으로
결과 읽은 뒤 files.delete 민감한 입력이 바로 사라진다 failed · expired · cancelled 경로를 빼먹기 쉽다 개인정보 · 계약서처럼 오래 두기 싫은 입력
주기 청소 Job 이미 쌓인 것까지 치운다 진행 중인 배치의 파일을 지우지 않게 확인이 필요하다 옛 파일 정리 · 안전망

나라면 만료를 먼저 건다. 코드 한 줄이고, 실패 경로를 빠뜨릴 일이 없다. 입력이 민감하면 삭제를 더한다. 삭제만 믿는 건 좀 불안하다. 예외 한 번에 파일이 남고, 그게 몇 달 뒤 장애가 되니까.

batch_file = client.files.create(
    file=open('chunk.jsonl', 'rb'),
    purpose='batch',
    extra_body={'expires_after': {'seconds': 1209600, 'anchor': 'created_at'}},  # 14일
)

batch = client.batches.create(
    input_file_id=batch_file.id,
    endpoint='/chat/completions',
    completion_window='24h',
    extra_body={'output_expires_after': {'seconds': 1209600, 'anchor': 'created_at'}},
)

숫자는 공식 문서 기준이다. seconds 는 1209600〜2592000(14〜30일)만 된다. 만료를 건 입력 파일 한도 10,000개를 14일로 나누면 하루 약 700개까지 버틴다. 출력 · 에러 파일은 이 한도에 안 들어가지만 기본 만료가 없어서, 그냥 두면 영원히 남는다. 그래서 output_expires_after 도 같이 건다.

한 가지. 이미 올린 파일에는 만료가 안 걸려 있으니 코드를 고쳐도 카운터는 안 내려간다(추정: 문서에 소급 얘기가 없다). 옛 파일은 직접 지워야 한다.

openai SDK 2.48.0 을 로컬 스텁 서버에 붙여 요청을 봤더니 extra_body 가 폼 필드 expires_after[seconds] 로 나갔다. 문서의 curl 예제는 expires_after.seconds 다. 직접 HTTP 로 부르면 표기가 다르니 문서 쪽을 따르자. 진짜 Azure 에 다시 올려 본 건 아니다.

4xx 를 500 으로 뭉개지 않기

파일 정리는 절반이다. 이번에 시간을 잡아먹은 건 중간 API 의 이 모양이었다.

# 고치기 전: 뭐가 와도 500
try:
    upload()
except Exception:
    raise HTTPException(500, 'Batch create failed')

# 고친 뒤: 상위 서비스의 상태와 코드를 넘긴다
try:
    upload()
except openai.APIStatusError as e:
    raise HTTPException(e.status_code, {'code': e.code, 'message': e.message})

한도가 3개인 스텁으로 두 코드를 돌리면 앞쪽은 HTTP 500, 뒤쪽은 400 quotaExceeded 를 낸다. 호출자가 이 코드 하나만 받았어도 조사는 몇 분이면 끝났을 거다. 다만 상위의 401 같은 건 그대로 넘기면 호출자가 자기 인증 문제로 오해한다. 그런 건 502 로 바꾸고 원래 코드만 본문에 싣는 편이 낫다.

그날 한 응급처치

순서가 중요하다. 먼저 배치가 전부 completed 이고 앱에서 결과를 기다리는 추출이 없는지 봤다. 그다음 입력 파일을 지우고, 이미 반영한 출력 파일을 지웠다. 합쳐서 천 개 남짓이다. 실패한 추출의 출력 파일은 조사용으로 남겼다. 마지막으로 프로덕션 컨테이너에서 테스트 파일을 올려 업로드가 되는 걸 보고 바로 지웠다.

놓치기 쉬운 것: 멈춘 동안 실패한 추출은 자동으로 다시 안 돌아간다. 화면에서 다시 눌러야 하니, 영향받은 조직에 알려야 한다.

OpenAI(Azure 아닌 쪽) 에도 같은 한도가 있는지는 확인 못 했다. 문서 페이지를 읽지 못했으니 직접 봐야 한다. 파일을 아예 Files API 에 안 올리는 길도 있다. Azure 는 Blob Storage 를 쓰는 Batch 를 따로 안내한다.

돌아가던 AI 기능이 이렇게 어느 날 멈췄는데 원인이 안 보인다면, negabaro 에 물어봐도 된다. 층을 하나씩 내려가며 원인을 같이 찾아 볼 수 있다.

핵심 개념

1

`500 ÷ 한 달 배치(청크) 수` 로 막히는 시점을 계산한다

2

업로드에 `expires_after`, 배치 생성에 `output_expires_after` 를 건다

3

이미 쌓인 파일은 진행 중인 배치가 없는지 보고 지운다

4

중간 API 는 상위 4xx 의 상태와 코드를 호출자에게 넘긴다

사용 사례

Azure OpenAI Batch 로 문서 · 계약서를 일괄 처리하는 기능 외부 API 를 감싸는 내부 API 의 에러 전달 설계
← 구조는 회로도, 가중치는 노브 위치 — 그리고 Attention 변형은 n² 을 피하려는 개조다 AI 에게 본번 DB 를 보여줄 때 — 쓰기는 프롬프트가 아니라 DB 권한으로 막는다 →