cron LLM 배치가 429 한 번 만나면 그 뒤로 줄줄이 죽어서 절반만 처리됩니다
수백 개 항목을 LLM으로 분류하는 배치를 매일 밤 cron으로 돌립니다. 항목마다 API 한 번씩 호출해요.
잘 돌다가도 중간에 429(rate limit) 한 번 터지면 그 뒤로 줄줄이 실패하면서 배치가 통째로 망가집니다. 다음 날 보면 절반만 처리돼 있고요. 그냥 time.sleep 넣어봤는데 너무 느려지고, 짧게 주면 또 429 납니다.
LLM API 배치에서 rate limit이랑 일시적 에러를 견디는 표준 재시도/동시성 패턴이 궁금합니다. 그리고 중간에 죽었을 때 처음부터 다시 안 돌리는 방법도요.
답변 2개
- 채택된 답변에디터 검증
이거 세 개를 같이 깔아야 합니다. 하나만 해선 안 돼요. 백오프 재시도 + 동시성 제어 + 체크포인트.
1. 지수 백오프 + jitter 429/5xx/타임아웃 만나면
2^n초로 늘려 재시도하되 거기에 랜덤 jitter를 꼭 더하세요. jitter 없으면 실패한 요청들이 동시에 깨어나서 또 같이 두드려서(thundering herd) 또 429 납니다. 그리고 응답에Retry-After헤더 오면 계산값 무시하고 그 값 따르는 게 맞아요. tenacity 같은 거 쓰면 데코레이터 한 줄입니다.@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6), retry=retry_if_exception_type(RateLimitError)) def classify(item): ...2. 동시성에 상한 semaphore로 동시 호출 수 묶으세요(5~10 정도부터).
sleep으로 직렬화하면 느리고, 무제한 병렬이면 429 폭발이라 그 사이 어딘가가 정답입니다. 토큰 기준 rate limit 걸리는 모델이면 요청 수만 보지 말고 토큰 예산도 같이 봐야 하고요.3. 체크포인트 — 이게 "처음부터 다시 안 돌리기"의 핵심 각 항목 결과를 처리하는 즉시 영속화하고(파일/DB/KV), 배치 시작할 때 이미 처리된 id를 스킵하세요. 이러면 중간에 죽어도 다음 실행이 남은 것만 이어 합니다. in-memory 리스트에 모았다가 끝에 한 번에 저장하는 구조면 중간에 죽을 때 다 날아가요. 반드시 항목 단위로 즉시 써야 합니다.
그리고 야간 분류처럼 즉시성 필요 없는 거면 아예 Batch API 보세요. OpenAI도 Anthropic도 비동기 배치 엔드포인트 있는데, rate limit 압박이 훨씬 덜하고 비용도 반값입니다. 실시간 아닌 야간 작업이면 설계상 이쪽이 더 맞아요 — 애초에 429 만날 일이 줄어드는 거라.
체크포인트 강조에 한 표. 저는 처리 결과를 그냥 JSONL에 append-only로 한 줄씩 씁니다. 단순한데 강력해요. 죽어도 거기까진 디스크에 남고, 재시작하면 이미 있는 id를 set으로 읽어서 스킵. DB 안 깔아도 되고 디버깅도 쉽고. 라인 파싱 깨질까 봐 항목 끝날 때마다
f.flush()까지 해두면 더 안전합니다.그리고 "통째로 죽는다"고 하셨는데, 한 항목 영구 실패(재시도 다 소진)가 배치 전체를 죽이면 그게 진짜 문제예요. 항목별로 try/except 감싸서 실패는 따로 실패 로그에만 적고 다음으로 넘어가게 하세요. 한 건 때문에 999건이 멈추는 구조는 무조건 고쳐야 합니다.