Anthropic 조직 한도를 다 써버리고 3일을 기다린 후, llmfleet를 구축했습니다
요약
Anthropic API의 조직 전체 일일 토큰 한도를 초과하여 72시간 동안 서비스 중단을 겪은 후, 이를 방지하기 위해 구축한 'llmfleet' 라이브러리를 소개합니다. llmfleet는 API 응답 헤더의 남은 토큰 정보를 실시간으로 관찰하여 동시성 및 토큰 사용량을 조절하는 백프레셔(backpressure) 메커니즘을 제공합니다.
핵심 포인트
- Anthropic API의 429 오류는 조직 전체의 일일 토큰 예산 초과 시 발생하며 해제에 긴 시간이 소요될 수 있음
- llmfleet는 단순한 세마포어 방식이 아닌, API 헤더의 'remaining-tokens'를 기반으로 한 협상형 디스패처 방식 채택
- soft_token_floor와 hard_token_floor 설정을 통해 토큰 잔여량에 따른 단계적 작업 중단 및 재개 가능
- 비동기 반복자(async iterator)를 통해 완료 순서대로 결과, 지연 시간, 예상 비용을 반환
화요일 오후, 저는 재채점(re-grading) 작업을 시작했습니다. claude-opus-4-7를 대상으로 약 18,000개의 프롬프트를 처리하기 위해 8명의 워커(worker)를 투입했고, 각 워커는 가능한 한 빠르게 messages.create를 반복 호출했습니다. 40분이 지나자 모든 호출이 429 오류와 함께 anthropic-ratelimit-tokens-remaining: 0 이라는 헤더를 반환하기 시작했습니다. 괜찮다고 생각하며 속도를 조절했습니다. 워커를 4명으로 줄이고 기다렸습니다. 여전히 429 오류가 발생했습니다. 2명으로 줄였습니다. 여전히 429였습니다. 그때 한도 해제(cap-clear) 타임스탬프가 몇 분 단위가 아니라는 것을 깨달았습니다. 그것은 계속 굴러가고 있었습니다. 저는 조직 전체의 일일 토큰 예산(daily token budget)을 초과해 버렸고, 일일 윈도우(daily window)는 5분 만에 리셋되지 않습니다. 고객 지원팀에 이메일을 보냈습니다. 그들은 수요일 아침에 확인 메일을 보냈습니다. 그리고 금요일 오후에야 한도를 해제해 주었습니다. 72시간이 걸린 것입니다. 그 이후의 엔지니어링 과정이 우아했다고 주장하지는 않겠습니다. 저는 3일 동안 대시보드를 새로고침하며 앉아 있었습니다. 마침내 한도가 해제되었을 때, 저는 다시는 그런 상황을 겪지 않기 위해 llmfleet를 구축했습니다.
llmfleet가 하는 일은 messages.create를 위한 풀링된 디스패처(pooled dispatcher)입니다. 메시지 페이로드(payload) 리스트와 동시성 한도(concurrency cap)를 전달하면, llmfleet는 다음 두 가지를 동시에 존중하는 백프레셔(backpressure)를 적용하여 작업을 실행합니다: 현재 진행 중인 요청 수(in-flight request count), 그리고 가장 최근의 anthropic-ratelimit-tokens-remaining 헤더입니다. Sandler에서 영감을 받은 핵심 부분은 협상(negotiation)입니다. 엄격한 세마포어(semaphore) 대신, 풀(pool)이 API가 알려주는 정보를 관찰합니다. 만약 남은 토큰(remaining-tokens) 헤더가 임계값 아래로 떨어지면, 윈도우가 넘어갈 때까지 진행 중인 슬롯(in-flight slots)을 유지합니다. 정신없는 429 재시도(retries)는 없습니다.
import asyncio
from llmfleet import Fleet
fleet = Fleet (
api_key = os.environ["ANTHROPIC_API_KEY"],
max_in_flight = 8,
soft_token_floor = 20_000, # 이 수치 아래로 떨어지면 새로운 디스패치 일시 중지
hard_token_floor = 2_000, # 다음 윈도우까지 완전 중단
)
payloads = [
{
"model": "claude-opus-4-7",
"max_tokens": 256,
"messages": [{"role": "user", "content": prompt}]
}
for prompt in prompts
]
async def run():
async for result in fleet.dispatch(payloads):
store(result.payload_id, result.response, result.cost_usd)
asyncio.
run ( run ()) dispatch는 제출 순서가 아닌 완료 순서대로 결과를 생성하는 비동기 반복자 (async iterator)입니다. 각 결과에는 원래의 페이로드 ID (payload id), 응답 (response), 밀리초(ms) 단위의 지연 시간 (latency), 그리고 예상 비용 (cost estimate)이 포함됩니다. 사람들이 물어볼 때 제가 인용하는 실제 수치들: 특별한 할당량 (quota)이 없는 단일 Anthropic 키를 기준으로: 짧은 프롬프트 (입력 토큰 약 400개, 출력 토큰 200개)의 경우, 소프트 플로어 (soft floor)가 작동하기 전까지 실무에서 관찰되는 초당 메시지 수 (Messages/sec) 상한선은 약 6.2 req/s입니다. 10분 동안 소프트 플로어에서 대기한 시간은 실제 경과 시간 (wall clock)의 약 11%였습니다. 하드 플로어 (hard floor)에서 일시 중지된 시간은 0이었습니다. 만약 soft_token_floor를 분당 토큰 할당량 (tokens-per-minute quota)의 약 10%로 설정한다면 말이죠. 그것이 바로 소프트 플로어의 존재 목적입니다. 더 높은 티어 (tier)의 할당량을 가지고 있다면 수치는 달라지겠지만, 그 형태는 동일합니다. 대기열 깊이 (Queue depth) 계산: 단순한 질문은 "max_in_flight를 얼마나 크게 설정해야 하는가?"입니다. Sandler의 답변은 리틀의 법칙 (Little's Law) 계산입니다. 평균 지연 시간 (latency)이 L초이고 원하는 처리량 (throughput)이 R req/s라면, 포화 상태에 도달하기 위해 최소 R*L개의 동시 호출 (concurrent calls)이 진행 중 (in flight)이어야 합니다. 200개 토큰 출력과 6 req/s의 일반적인 4초 응답을 보이는 Claude Opus의 경우, 이는 24개의 in-flight 호출입니다. 하지만 대부분의 계정에서 적용되는 Anthropic의 분당 제한 (per-minute limit)이 그전에 당신을 가로막을 것입니다. 따라서 실제 max_in_flight는 min(R*L, perminute_quota / 60 * L) 입니다. llmfleet는 tier="default" 또는 당신의 티어에 맞는 값을 전달하면 이 계산을 대신 해줍니다. 그리고 시작 시 선택된 상한선 (ceiling)을 로그로 남깁니다. 중요했던 작은 디테일: 저를 이 문제에 빠뜨렸던 원래의 429 재시도 (retry)는 악의적인 것이 아니었습니다. 그것은 SDK가 기본적으로 수행하는 지수 백오프 (exponential backoff)였습니다. 모든 워커 (worker)가 독립적으로 백오프를 수행하고 다시 실행되었고, 이로 인해 실제 작업이 유휴 (idle) 상태가 된 후에도 몇 시간 동안 상한선이 0에 고정되어 있었습니다. llmfleet는 SDK의 내부 재시도를 비활성화합니다. 재시도 예산 (retry budget)은 풀 (pool)이 관리합니다. 하나의 공유된 카운트입니다. 단일 요청이 재시도 불가능하게 실패할 경우, 풀은 이를 사용자에게 노출할지 아니면 넘어갈지를 결정할 수 있으며, 디스패처 (dispatcher)는 실패한 시도의 비용을 로그로 남겨 예산 추적 (budget tracking)에서 누락되지 않도록 합니다.
fleet = Fleet ( api_key = ..., retry_policy = dict ( max_attempts = 3 , base_delay = 2.0 , max_delay = 30.0 ), shared_retry_budget_per_min = 20 )
비용 보호 (Cost guard)
또한 저는 새벽 2시의 제 자신을 믿지 못하기 때문에 하드 USD 한도 (hard USD cap)를 추가했습니다.
fleet = Fleet ( api_key = ..., max_spend_usd = 15.00 )
누적 합계가 한도를 초과하면, 새로운 디스패치 (dispatch)는 나가지 않습니다. 진행 중인 (In-flight) 작업들은 여전히 완료됩니다. 이터레이터 (iterator)는 마지막으로 BudgetExceeded 마커를 반환하고 중단됩니다.
이것이 해결하지 못하는 것
이것은 계정 할당량 (account quota)을 높여주지는 않습니다. 3일 동안 기다려야 했던 것은 코드 문제가 아니라 할당량 문제였습니다. llmfleet는 당신이 한도를 넘지 않도록 관리해 줄 뿐, 한도를 넘겨주지는 않습니다.
현재는 Anthropic하고만 통신합니다. 인터페이스는 messages.create와 정확히 일치합니다. OpenAI로 일반화할 수도 있지만, 아직은 하지 않았습니다.
프롬프트 캐싱 (prompt caching)을 대신 해주지는 않습니다. 그것을 원하신다면 cachebench를 살펴보세요. 두 가지는 상호 보완적입니다: 캐싱은 한도에 대해 계산되는 토큰 수를 줄여줍니다.
우선순위 차선 (priority lanes)을 구현하지는 않습니다. 모든 페이로드 (payload)는 FIFO (First-In-First-Out) 방식입니다. 특정 작업이 대기열을 건너뛰길 원한다면, 두 개의 fleet을 실행하세요.
라이브러리 전체는 약 700줄 정도입니다. 흥미로운 부분은 대기열 (queue)이 아니라 하한선 (floor) 로직입니다.
Repo: https://github.com/MukundaKatta/llmfleet
PyPI: pip install llmfleet
실제 사고들로부터 계속해서 만들어가고 있는 에이전트 배관 (agent-plumbing) 라이브러리 스택의 일부입니다. 화려하지는 않은 것들이죠.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기