매 분 정각에 문서 처리 에이전트 열 개가 뜨고, 로그에는 429 Too Many Requests 가 줄지어 찍혀요. 콘솔의 토큰 사용량은 분당 한도의 절반 근처고요. 한도를 넘은 적이 없는데 한도 초과라고 해요.
고객사 사고가 아니라 가짜 API 로 재현한 장면이에요. 제공자 문서의 집행 방식을 흉내 낸 한도(분당 토큰 30만 · 요청 300건)에, 정각마다 작업 10개가 뜨고 각각 다섯 턴을 도는 구조를 10분 흘렸어요.
처음엔 재시도만 제대로 붙이면 될 줄 알았어요
처음 상태(A)는 max_tokens 4,096 에 429 면 0.2초 뒤 바로 다시 보내고 세 번이면 포기하는 설정이에요. 10분 동안 429 가 200번, 작업 100개 중 50개는 첫 턴도 못 넘기고 죽었어요.
그래서 retry-after 를 지키는 지수 백오프로 바꿨어요(B). 429 는 735번으로 늘었어요. 재시도를 잘 붙인 게 아니라, 거절당할 요청을 더 오래 더 많이 보낸 거였어요.
| 429 | 재시도 | 완료 | 작업 p95 | 과금 토큰/분 (한도 대비) | |
|---|---|---|---|---|---|
| A 그대로 — max_tokens 4,096 · 즉시 재시도 3회 | 200 | 150 | 50/100 | 22초 | 95,000 (32%) |
| B 지수 백오프 | 735 | 716 | 81/100 | 300초 | 142,080 (47%) |
| C B + max_tokens 512 | 50 | 50 | 100/100 | 24초 | 190,000 (63%) |
| D C + 클라이언트 예산 게이트 | 0 | 0 | 100/100 | 24초 | 190,000 (63%) |
| E 게이트만 (max_tokens 4,096 유지) | 0 | 0 | 100/100 | 246초 | 142,960 (48%) |
한도를 먹는 건 응답이 아니라 max_tokens 였어요
Azure OpenAI 문서는 요청이 도착하는 순간 프롬프트 추정치 + max_tokens 를 분 단위 카운터에 더하고, 한도에 닿으면 그 분이 끝날 때까지 429 라고 적어요. 과금은 처리 뒤 실제 토큰으로 따로 세고요. OpenAI 문서도 max_tokens 와 추정치 중 큰 값이라고 해요.
저희 작업은 턴마다 프롬프트가 1,500 에서 5,500 토큰으로 자라고 응답은 300 토큰이에요. max_tokens 가 4,096 이니 작업 하나의 과금은 19,000 인데 차감은 37,980, 꼭 두 배예요. 열 개면 379,800 으로 한도를 넘어요. 그래프의 "절반"은 과금 토큰이었고, 429 를 정하는 건 추정치였어요. 이번 분에 밀린 요청이 다음 분의 열 개와 같이 차감되니, 한 번 밀리면 매 분 밀려요.
1분 한도는 1초 단위로 집행돼요
두 번째 자는 요청 수예요. Azure 문서는 분당 600건을 1초에 10건으로 평가한다고 적고, Anthropic 문서도 60 RPM 이 1초에 1건으로 집행될 수 있다고 해요. 저희 설정은 1초에 5건이라 정각에 열 개가 같은 초에 도착하면 다섯은 거절돼요. 분당으로는 50건, 한도의 6분의 1인데도요.
A 의 즉시 재시도가 전부 죽은 이유가 이거예요. 0.2초 뒤는 같은 1초 창이고, OpenAI 와 Azure 문서 둘 다 실패한 요청도 분당 한도에 센다고 적어 뒀어요.
고친 건 두 가지예요 — max_tokens 는 현실로, 요청은 게이트로
먼저 max_tokens 를 응답 크기에 맞춰 512 로 내렸어요(C). 토큰 쪽 429 는 사라졌고, 남은 50번은 전부 정각 버스트의 초 단위 429 였어요.
그 버스트는 제공자가 세는 방식 그대로 세는 게이트를 클라이언트에 둬서 없앴어요(D). 보내기 전에 이번 초와 이번 분의 예산을 보고 줄을 세우고, 추정치도 제공자 공식대로 프롬프트 + max_tokens 로 넣어요.
class TokenBudgetGate:
def __init__(self, rps: int, tpm: int):
self.rps, self.tpm = rps, tpm
self.per_sec, self.per_min = {}, {}
async def admit(self, est_tokens: int): # est = prompt + max_tokens
while True:
now = time.time(); sec, minute = int(now), int(now // 60)
if self.per_sec.get(sec, 0) >= self.rps:
await asyncio.sleep(sec + 1 - now); continue # 다음 초까지
if self.per_min.get(minute, 0) + est_tokens > self.tpm:
await asyncio.sleep((minute + 1) * 60 - now); continue # 다음 분까지
self.per_sec[sec] = self.per_sec.get(sec, 0) + 1
self.per_min[minute] = self.per_min.get(minute, 0) + est_tokens
return
달라진 건 429 가 0 인데 작업 완료 시간은 그대로(p95 24초)라는 점이에요. 정각의 열 건이 2초에 걸쳐 나뉘어 나갈 뿐이거든요. 반대로 max_tokens 4,096 을 둔 채 게이트만 붙이면(E) 429 는 0 이지만 p95 가 246초예요. 한도를 먹는 게 추정치라 줄이는 것도 추정치여야 했어요.
제공자마다 자가 달라요
| Azure OpenAI | OpenAI | Anthropic | |
|---|---|---|---|
| 토큰 차감 | 도착 시 프롬프트 추정치 + max_tokens, 분 단위 리셋 | max_tokens 와 추정치 중 큰 값 | 입력은 시작 시 추정 후 실제로 보정, 출력은 생성된 만큼. max_tokens 불산입 |
| 캐시 | 문서에 기재 없음 | 문서에 기재 없음 | 캐시 읽기 토큰은 입력 한도에 불산입 (대부분 모델) |
| 창 | RPM 은 1초 · 10초 창 | 급증 시 slow_down 429 | 토큰 버킷, 연속 보충. 60 RPM 이 1초 1건일 수 있음 |
| 헤더 | x-ratelimit-* · retry-after-ms | x-ratelimit-* · Retry-After | anthropic-ratelimit-* · retry-after |
Anthropic 은 캐시에 올린 시스템 프롬프트가 한도에서 빠져요. 토큰 비용 글의 프롬프트 캐싱이 한도에도 걸려 있는 셈이에요.
바로 믿으면 안 되는 세 가지
하나, 숫자는 시뮬레이션이에요. 한도 · 작업 구조 · 지연은 가정값이고, 실제 제공자의 추정은 문자 수 기반이라 더 거칠어요.
둘, 429 가 전부 한도 초과는 아니에요. Azure 문서는 용량 부족과 공유 풀 보호용 임시 한도 축소를 따로 적어요. 헤더의 x-ratelimit-limit-tokens 가 설정값보다 낮으면 그쪽이라, 쿼터 증설 요청은 번지수가 틀려요.
셋, 게이트는 프로세스 하나 안에서만 정확해요. 워커가 파드 여러 개면 각자 센 예산의 합이 한도를 넘어요. 모델 교체 글에서 둔 LLM 게이트웨이에서 한 번만 세야 하는데, 이번 재현엔 넣지 못했어요.
몇 번 다시 보낼지가 아니라 누가 세고 있는지였어요
재시도 정책은 거절당한 뒤의 일이에요. 거절당하기 전에 제공자와 같은 자로 세는 쪽이 먼저였어요.
멱등성 글에서 "이 도구를 다시 불러도 되는가"를 물었다면, 이번엔 "이 요청을 지금 보내도 되는가"예요. 완성된 규칙은 아니에요. 지금은 제공자마다 차감 공식을 한 줄씩 적어 두고, 그 줄이 바뀔 때마다 게이트를 고치고 있어요.
자주 묻는 질문
Q. 한도를 올려 달라고 하면 되지 않나요? A. 과금 토큰이 한도의 절반이라면 세는 방식의 문제라, 올려도 비율만 달라져요.
Q. SDK 의 자동 재시도만으로 부족한가요? A. 보내기 전의 게이트와 응답 크기에 맞춘 max_tokens 가 먼저예요.
Q. max_tokens 를 줄이면 응답이 잘리지 않나요? A. 잘릴 수 있어요. 응답 길이 분포를 재고 상위 값에 여유를 더해 정하고, 종료 사유가 길이 초과로 온 경우만 올려서 한 번 더 불러요.
Sources
- Microsoft Learn — Manage Azure OpenAI in Microsoft Foundry Models quota (Understanding rate limits · 429 troubleshooting), 2026-05-04: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/quota
- OpenAI Developers — Rate limits: https://developers.openai.com/api/docs/guides/rate-limits
- Claude Developer Platform — Rate limits: https://platform.claude.com/docs/en/api/rate-limits
본문 수치는 이 회차에서 직접 돌린 시뮬레이션 결과예요. 실제 LLM 호출과 고객사 데이터는 쓰지 않았고, 제공자 문서의 집행 방식을 흉내 낸 가짜 한도(300,000 TPM · 300 RPM) 에 작업 100개 · seed 20261026 으로 실행했습니다. 한도 · 작업 구조 · 지연 시간은 가정값이에요.

