본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 10. 19:14

Rate Limiting, Retry Logic, 그리고 직접 만든 Claude Proxy가 요청을 조용히 누락시키는 이유

요약

Claude Proxy 구축 시 발생하는 요청 누락 문제의 원인인 Anthropic의 RPM/TPM 속도 제한 메커니즘을 분석합니다. 단순 재시도 로직이 초래하는 트래픽 충돌 현상과 이를 해결하기 위한 올바른 접근법을 다룹니다.

핵심 포인트

  • Anthropic API는 RPM과 TPM 두 가지 차원의 속도 제한을 적용함
  • 단순 재시도 패턴은 동기화된 트래픽 돌진(Stampede)을 유발함
  • 429 에러 발생 시 지수 백오프(Exponential Backoff) 전략이 필수적임
  • 사용자별 요청이 하나의 API 키 버킷을 공유함을 유의해야 함

Rate Limiting, Retry Logic, 그리고 직접 만든 Claude Proxy가 요청을 조용히 누락시키는 이유

당신은 Claude 프록시(Proxy)를 구축했습니다. 요청을 전달하고, 인증을 처리하고, 응답을 반환하는 것 — 꽤 간단해 보입니다. 개발 단계에서는 잘 작동합니다. 몇 번의 호출로 테스트했을 때는 모든 것이 완벽해 보이죠.

그러다 실제 사용자를 추가하게 됩니다.

갑자기 요청들이 실패하기 시작합니다. 모든 요청이 그런 것도 아니고, 예측 가능한 것도 아니며, 그저 가끔씩 발생합니다. 일부 사용자는 응답을 받지 못했다고 보고합니다. 로그에는 에러가 찍히지만 일관적이지는 않습니다. 실제 유입되는 트래픽에 비해 사용량이 예상보다 낮습니다. 무언가가 요청을 누락시키고 있는데, 무엇인지 알 수 없습니다.

그 답은 거의 항상 속도 제한 (Rate Limiting) — 그리고 당신의 재시도 로직 (Retry Logic)이 고장 났거나, 없거나, 혹은 상황을 더 악화시키고 있다는 사실에 있습니다.

Anthropic의 속도 제한 (Rate Limits)이 실제로 작동하는 방식

Anthropic의 API는 두 가지 중첩된 속도 제한 차원을 사용합니다: **분당 요청 수 (RPM, requests per minute)**와 **분당 토큰 수 (TPM, tokens per minute)**입니다. 이들은 별개의 카운터이며, 둘 중 어느 하나라도 독립적으로 한도에 도달할 수 있습니다.

티어(Tier)는 대략 다음과 같습니다:

  • Free tier: 5 RPM, 25,000 TPM
  • Tier 1 (~$100 누적 지출): 50 RPM, 50,000 TPM
  • Tier 2 (~$500 누적 지출): 1,000 RPM, 100,000 TPM
  • Tier 3 (~$5,000 누적 지출): 2,000 RPM, 200,000 TPM
  • Tier 4 (~$25,000 누적 지출): 4,000 RPM, 400,000 TPM

프록시를 운영하기 전까지는 이 수치들이 여유로운 헤드룸(Headroom)처럼 보일 것입니다. 하지만 프록시에 여러 사용자가 있을 때, 이들은 별개의 속도 제한 풀(Pool)이 아닙니다. 모두 당신의 API 키에 연결된 동일한 버킷(Bucket)에서 가져다 쓰는 것입니다.

적당한 요청을 보내는 다섯 명의 사용자만 있어도 몇 초 만에 RPM 제한을 초과할 수 있습니다. 긴 문서를 요약하려는 한 명의 사용자가 다른 사용자가 기회를 잡기도 전에 TPM 예산을 다 써버릴 수도 있습니다. 그리고 Anthropic은 우선순위 큐(Priority Queue) 없이 429 Too Many Requests 응답을 반환합니다. 즉, 그냥 거절하는 것입니다.

429 문제: 대부분의 프록시가 실수하는 것

속도 제한에 걸려 429 에러를 받으면, 올바른 조치는 기다렸다가 재시도하는 것입니다. 단순한 구현 방식은 정확히 그렇게 동작하지만 — 구현을 잘못하고 있습니다.

**단순 재시도 패턴 (The flat retry pattern)**은 가장 흔한 실수입니다:

for attempt in range(3):
    response = call_claude(request)
    if response.status_code == 429:
...

이것은 합리적으로 보입니다. 하지만 부하가 걸린 상황에서는 실제로 재앙이 됩니다.

이유는 다음과 같습니다. 만약 10개의 요청이 동시에 속도 제한 (Rate Limit)에 걸렸고, 모두 정확히 1초 후에 재시도한다면, 그들은 모두 동시에 재시도하게 됩니다. 당신은 트래픽 충돌을 T+0에서 T+1로 옮겼을 뿐입니다. 만약 재시도마저 실패한다면 (1초 만에 속도 제한 윈도우 (Rate Limit Window)가 해제되지 않으므로 반드시 실패할 것입니다), 그들은 모두 T+2에 다시 재시도합니다. 당신은 동기화된 대규모 돌진 (Synchronized stampede)을 만들어낸 것입니다.

**지수 백오프 패턴 (The exponential backoff pattern)**은 타이밍 문제를 해결합니다:

for attempt in range(5):
    response = call_claude(request)
    if response.status_code == 429:
...

더 나아졌습니다. 하지만 여전히 결정적인 무언가가 빠져 있습니다.

**지터(Jitter)를 포함한 지수 백오프 패턴 (The exponential backoff with jitter pattern)**이 실제로 작동하는 방식입니다:

import random

for attempt in range(5):
...

지터 (Jitter)는 재시도를 동기화하는 대신 특정 시간 범위 내에 분산시킵니다. 이제 10개의 실패한 요청은 모두 동시에 몰리는 대신, 특정 범위 내에서 10개의 서로 다른 시간에 재시도하게 됩니다.

지터가 없다면, 지수 백오프 (Exponential backoff)는 단지 당신의 천둥 치는 들소 문제 (Thundering herd problem)를 더 큰 시간 간격으로 옮길 뿐입니다.

조용히 누락되는 요청 (Silent Drops): 당신이 로깅하지 않고 있는 실패 모드

가장 포착하기 어려운 실패 모드는 다음과 같습니다: 성공한 것처럼 보이지만 비어 있거나 부분적인 결과만 반환하는 요청입니다.

이는 몇 가지 방식으로 발생합니다:

불완전한 응답 처리 (Incomplete response handling). Claude의 API는 스트리밍 응답 (Streaming responses)을 지원합니다. 만약 클라이언트 타임아웃 (Client timeout), 네트워크 중단, 또는 잘못 설정된 프록시 타임아웃 (Proxy timeout)으로 인해 프록시가 스트림이 완료되기 전에 연결을 끊어버리면, 클라이언트는 부분적인 응답을 받게 됩니다. API 관점에서는 요청이 "성공"한 것입니다. 당신의 로그에는 200 상태 코드가 찍힙니다. 하지만 사용자는 잘린 출력을 받게 됩니다.

Silent 529 handling (조용한 529 처리). Anthropic은 가끔 529 Overloaded를 반환합니다. 이는 속도 제한 (Rate Limit)이 아니라 서버 용량 문제입니다. 만약 당신의 재시도 로직 (Retry Logic)이 429는 처리하지만 529를 처리하지 않는다면, 과부하 응답은 재시도되는 대신 사용자에게 에러로 노출됩니다.

Token budget exhaustion mid-stream (스트림 중간의 토큰 예산 소진). 프록시에서 토큰 예산 (Token Budget)을 강제하고 있는데 응답이 스트림 중간에 이를 초과하는 경우, 버퍼링 후 잘라내거나(buffer and truncate) 절단 상황을 우아하게 처리해야 합니다. 많은 DIY 구현체들은 그냥... 전송을 중단해 버립니다. 사용자는 문장 중간에 끊겨버린 응답을 보게 됩니다.

Connection pool exhaustion under load (부하 상황에서의 커넥션 풀 고갈). 커넥션 풀링 (Connection Pooling)이 제대로 설정되지 않은 프록시를 실행 중이라면, 동시 요청들이 서로를 고갈시킬 수 있습니다. 요청들이 커넥션 슬롯을 기다리며 큐 (Queue)에 쌓이다가, 클라이언트 측 타임아웃 (Client-side Timeout)에 걸려 드롭됩니다. API는 이 요청들을 본 적조차 없습니다. 요청들은 당신의 프록시 커넥션 큐에서 죽어버린 것입니다.

이 모든 현상은 로그에서 동일한 방식으로 나타납니다. 요청이 시작되었고, 아마도 짧은 외부 호출이 있었을 것이며, 에러도 없고, 완료도 되지 않은 상태 말입니다.

Multi-User Queue Starvation (다중 사용자 큐 기아 현상)

이것은 팀이 단일 사용자 환경에서 다중 사용자 환경으로 넘어갈 때 직면하게 되는 실패 모드입니다.

당신의 프록시가 선착순 (First-come-first-served) 방식으로 요청을 처리하고 있고, RPM (Requests Per Minute) 제한에 근접해 있다고 가정해 봅시다. 한 명의 사용자가 자동 완성, 긴 문서 분석, 몇 가지 명확화 질문 등 일련의 요청을 빠르게 보냅니다. 이 사용자가 현재 윈도우 (Window) 내에서 사용 가능한 요청 예산의 대부분을 소비합니다.

이제 다른 사용자들의 요청은 큐에 쌓입니다. 그들은 기다리고 있습니다. 그중 일부는 속도 제한 윈도우가 재설정되기 전에 타임아웃이 발생합니다. 영향을 받은 사용자들은 에러를 받는 것이 아니라, 그냥 응답을 받지 못합니다. 그들의 관점에서는 프록시가 간헐적으로 고장 난 것처럼 보입니다.

이것이 바로 큐 기아 (Queue Starvation) 현상이며, 로그에 사용자별 요청 귀속 정보 (Per-user request attribution)가 없다면 진단하는 것이 거의 불가능합니다. 만약 당신이 timestamp + status + latency는 기록하고 있지만 user_id + queue_time을 기록하지 않고 있다면, 개별 사용자들은 고통받고 있음에도 전체적인 속도 (Rate)는 정상인 것으로 보이게 될 것입니다.

해결책은 요청이 Anthropic의 API에 도달하기 전, 프록시 계층(proxy layer)에서 사용자별 속도 제한(per-user rate limiting)을 포함한 공정 큐잉(fair queuing)을 구현하는 것입니다. 이를 올바르게 구현하는 것은 결코 간단하지 않습니다. 단순한 방식(사용자 간 라운드 로빈(round-robin))은 서로 다른 요청 크기나 우선순위를 고려하지 못합니다. 제대로 된 구현을 위해서는 토큰 인지 스케줄링(token-aware scheduling)이 필요합니다.

올바른 프록시 속도 제한(Rate Limiting)에 실제로 필요한 것들

속도 제한을 올바르게 처리하는 DIY 프록시를 구축하고 싶다면, 최소한 다음 사항들이 필요합니다:

1. 사용자별 요청 추적 (Per-user request tracking). 모든 요청을 사용자 ID, 타임스탬프(timestamp), 입력 토큰(input tokens), 출력 토큰(output tokens), 그리고 대기 시간(queue time)과 함께 기록하십시오. 이것이 다른 모든 것의 기초가 됩니다.

2. 토큰 인지 스케줄링 (Token-aware scheduling). 단순히 요청 횟수만 세지 마십시오. 스케줄링할 때 각 요청의 토큰 가중치를 고려해야 합니다. 2,000 토큰 요청은 100 토큰 요청과 동일한 RPM(Requests Per Minute) 슬롯을 소비하지만, TPM(Tokens Per Minute) 예산은 20배를 소비합니다.

3. 지터를 포함한 적절한 재시도 (Proper retry with jitter). 위에서 설명한 바와 같이, 429 및 529 응답을 모두 처리하면서 무작위 지터(randomized jitter)를 적용한 지수 백오프(exponential backoff)를 사용하십시오.

4. 서킷 브레이커 (Circuit breakers). Anthropic의 API가 지속적으로 에러를 반환한다면, 계속해서 요청을 퍼붓는 것을 멈추고 상황이 해제될 때까지 빠르게 실패(fail fast) 처리하십시오. 서킷 브레이커 패턴은 일시적으로 성능이 저하된 업스트림(upstream)이 프록시 내에서 연쇄적인 장애(cascading failures)를 일으키는 것을 방지합니다.

5. 기아 현상 방지를 포함한 공정 큐잉 (Fair queuing with starvation prevention). 단일 사용자가 공유 용량 예산을 모두 소진할 수 없도록 프록시 계층에서 일종의 사용자별 속도 제한을 구현해야 합니다.

6. 스트림 무결성 모니터링 (Stream integrity monitoring). 스트리밍 응답을 추적하여 불완전한 전달을 감지하십시오. 자연스러운 완료 신호가 오기 전에 스트림이 종료된다면, 이를 플래그(flag) 처리하고 기록하며, 선택적으로 재시도하십시오.

7. 현실과 일치하는 타임아웃 설정 (Timeout configuration that matches reality). Claude의 긴 요청은 30~60초가 걸릴 수 있습니다. 만약 프록시의 타임아웃이 15초로 설정되어 있다면, 여러분은 정당한 요청의 상당 부분을 조용히 누락시키고 있는 것입니다.

이것은 올바르게 구축하고 유지 관리하기 위해 결코 만만치 않은 양의 인프라입니다. 그리고 이는 테스트 단계(트래픽 패턴을 직접 제어할 수 있는 환경)에서는 괜찮아 보이지만, 프로덕션 환경(제어할 수 없는 환경)에서는 무너지는 종류의 문제입니다.

시간이 지날수록 상황이 악화되는 이유

DIY 프록시 문제는 안정화되기보다는 복합적으로 악화되는 경향이 있습니다.

새로운 사용자를 추가합니다. 이제 큐 기아 (Queue Starvation) 현상이 처음으로 1번 사용자에게 가끔 발생합니다. 이를 인지하고 임시방편 (Band-aid)을 추가합니다. 또 다른 사용자를 추가합니다. 임시방편이 버티지 못합니다. 다시 패치를 적용합니다.

더 긴 출력을 생성하는 새로운 기능을 추가합니다. 이제 이전에는 겪지 않았던 TPM (Tokens Per Minute) 제한에 걸리기 시작합니다. 기존의 재시도 로직 (Retry Logic)은 RPM (Requests Per Minute) 429 에러는 처리하지만, TPM 429 에러는 처리하지 못합니다 (Anthropic은 각각에 대해 서로 다른 에러 메시지를 반환합니다). 이를 수정합니다.

한 사용자가 짧은 요청을 매우 빠른 속도로 연속해서 대량 생성하는 유스케이스 (Use case)를 찾아냅니다. 이제 RPM 제한이 제약 사항이 됩니다. 여러분의 토큰 인식 스케줄러 (Token-aware scheduler)는 이 패턴을 고려하지 못합니다. 이를 조정합니다.

각각의 수정 사항은 합리적입니다. 하지만 누적된 결과는 여러 겹의 패치가 쌓인 취약한 시스템이며, 각 패치는 서로에게 의존하고 있고, 그 중 어느 것도 실제 프로덕션에서 나타나는 결합된 부하 패턴에 대해 테스트되지 않았습니다.

결국 여러분 스스로에게 던지게 될 수학적인 질문은 이것입니다: 이 일을 위해 얼마나 많은 엔지니어링 시간을 소비했는가? 사용자가 실제로 원하는 기능을 출시하지 못함으로써 발생하는 기회비용은 얼마인가?

관리형 대안 (The Managed Alternative)

ShadoClaw가 존재하는 이유는 이러한 인프라 문제가 이미 해결되어 있으며, Claude를 안정적으로 운영하려는 모든 팀이 이를 매번 다시 구축할 필요가 없어야 하기 때문입니다.

프록시 계층이 Anthropic의 속도 제한 (Rate limits), 재시도 로직 (Retry logic), 공정 큐잉 (Fair queuing), 스트림 무결성 (Stream integrity), 그리고 연결 관리 (Connection management)를 인프라 수준에서 처리합니다. 여러분은 이러한 메커니즘을 직접 구축하고 유지 관리하지 않고도 예측 가능한 동작을 얻을 수 있습니다.

가격은 티어별 고정 요금제입니다:

  • Solo ($29/month): 계정 1개. 예측 가능한 월간 비용.
  • Pro ($79/month): 계정 5개. 소규모 팀과 에이전시에 적합.
  • Team ($179/month): 계정 20개. 대규모 프로덕션 워크로드 (Production workloads).

토큰당 과금에 대한 불안감이 없습니다. 사용자 두 명을 추가했다고 해서 "재시도 로직 (retry logic)을 수정하기 위해 엔지니어링 스프린트를 돌리는" 일도 없습니다. 속도 제한 (Rate limiting)의 복잡성은 처리되었으니, 여러분은 구축하고 있는 것에만 집중하세요.

모든 플랜에는 3일 무료 체험이 포함되어 있습니다. 만약 직접 만든 프록시 (DIY proxy) 설정에서 간헐적인 실패를 추적하고 있었다면, 가장 빠른 진단 방법은 ShadoClaw를 통해 트래픽을 라우팅하여 해당 문제가 사라지는지 확인하는 것입니다.

ShadoClaw는 AI, Web3, 그리고 SaaS 인프라를 전문으로 하는 IT 엔지니어링 스튜디오인 Gerus-lab에서 구축하고 유지 관리합니다.

다음 배포 전 실무 체크리스트

당분간 직접 구축 (DIY) 방식을 유지할 계획이라면, 사용자를 더 추가하기 전에 다음 사항들을 점검하십시오:

  1. 재시도 로직 (retry logic)에 지터 (jitter)를 사용하고 있습니까? 사용하고 있지 않다면, 다른 무엇보다 먼저 지터를 추가하십시오.
  2. 429와 529 오류를 모두 처리하고 있습니까? 두 상태 코드 (status codes) 모두에 대해 명시적으로 오류 처리 (error handling)를 확인하십시오.
  3. 타임아웃 (timeouts) 설정이 Claude의 실제 응답 시간과 일치합니까? 예상되는 가장 긴 프롬프트로 테스트하고, 그보다 20% 높게 타임아웃을 설정하십시오.
  4. 모든 요청에 사용자 ID와 대기 시간 (queue time)을 기록하고 있습니까? 이것이 없다면 기아 현상 (starvation)은 눈에 보이지 않습니다.
  5. 프록시 계층 (proxy layer)에서 사용자별 속도 제한 (per-user rate limiting)을 적용하고 있습니까? 아니면 한 명의 사용자가 모든 용량을 소비할 수 있습니까?
  6. 스트리밍 응답 (streaming responses)이 완료되었는지 검증하고 있습니까? 아니면 부분적인 응답이 사용자에게 조용히 전달될 수 있습니까?

이 사항들을 순서대로 해결하십시오. 모든 항목의 영향력이 동일한 것은 아닙니다. 재시도 지터 (retry jitter) 문제 하나만으로도 다중 사용자 환경에서 발생하는 상관관계가 있는 실패의 불균형적으로 큰 비중을 차지합니다.

문제는 해결 가능합니다. 질문은 이 문제들을 직접 해결하는 것이 여러분의 시간을 사용하는 올바른 방법인가 하는 점입니다.

ShadoClaw — 관리형 Claude API 프록시. 정액제 요금제. 3일 무료 체험.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0