- Published on
Anthropic Claude 429 Rate Limit 실무 재시도 패턴
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Claude를 붙이다 보면 가장 자주 만나는 장애 중 하나가 429 Rate Limit입니다. 문제는 “재시도하면 되지”라고 접근하면, 재시도가 다시 429를 만들고, 그 재시도가 또 재시도를 낳는 자기증폭(thundering herd) 으로 번지기 쉽다는 점입니다. 특히 동시 요청이 많은 API 서버, 배치 워커, 큐 컨슈머에서 이 현상이 빠르게 확산됩니다.
이 글에서는 Claude 429를 실무에서 안전하게 재시도하기 위한 원칙과 구현 패턴을 정리합니다. 핵심은 3가지입니다.
- 429를 “일시적 실패”로만 보지 말고 용량(capacity) 신호로 해석하기
- 재시도는 헤더 기반 대기 + 지터 백오프 + 동시성 제어를 함께 적용하기
- 재시도만으로 해결되지 않으면 요청 구조를 바꿔 (배칭, 캐시, 큐잉) 근본적으로 압력을 낮추기
429가 의미하는 것: 단순 에러가 아니라 용량 신호
429는 보통 “너무 많이 요청했다”이지만, 운영 관점에서는 다음을 동시에 의미합니다.
- 현재 테넌트/키의 분당 요청 수(RPM) 또는 토큰 처리량(TPM) 을 초과
- 순간적으로 트래픽이 몰려 버스트 한도를 넘김
- 동일한 시간창에서 재시도가 겹치며 동시성 폭발
따라서 재시도 전략은 “몇 번 더 던져보자”가 아니라 다시 시도해도 되는 시점까지 기다리는 로직이 되어야 합니다.
재시도 설계의 기본: 헤더 우선, 그 다음 백오프
Claude를 포함한 많은 API는 429에서 “언제 다시 요청하라”는 정보를 헤더로 제공합니다. 실무에서는 다음 우선순위를 추천합니다.
- 응답 헤더에
retry-after또는 유사 헤더가 있으면 그 값을 최우선으로 사용 - 없으면 지수 백오프(exponential backoff) + 지터(jitter)
- 그래도 계속 429면 동시성 제한 또는 큐잉으로 구조 변경
왜 지터가 필수인가
지수 백오프만 쓰면 여러 요청이 같은 시점에 실패하고 같은 시점에 재시도하여 다시 같은 시점에 실패합니다. 지터는 재시도 시점을 분산시켜 “동시에 몰리는 재시도”를 깨줍니다.
Node.js에서 바로 쓰는 429 재시도 래퍼
아래 예시는 Node 런타임에서 Claude 호출 함수를 감싸는 방식입니다.
- 429일 때만 재시도
retry-after가 있으면 그만큼 대기- 없으면 지수 백오프 + 지터
- 최대 재시도 횟수와 최대 대기 상한을 둠
type RetryOptions = {
maxRetries: number;
baseDelayMs: number; // 예: 300
maxDelayMs: number; // 예: 10_000
};
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
function parseRetryAfterMs(headers: Headers): number | null {
// `retry-after`는 초 단위 또는 HTTP date일 수 있음
const v = headers.get("retry-after");
if (!v) return null;
const asNumber = Number(v);
if (!Number.isNaN(asNumber)) return Math.max(0, asNumber * 1000);
const asDate = Date.parse(v);
if (!Number.isNaN(asDate)) return Math.max(0, asDate - Date.now());
return null;
}
function backoffWithJitterMs(attempt: number, baseDelayMs: number, maxDelayMs: number) {
const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
// Full jitter: 0..exp
return Math.floor(Math.random() * exp);
}
async function withRateLimitRetry<T>(
fn: () => Promise<T>,
opts: RetryOptions
): Promise<T> {
let lastErr: unknown;
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
lastErr = err;
// 라이브러리마다 에러 형태가 다르므로 status 추출을 표준화하는 게 중요
const status = err?.status ?? err?.response?.status;
const headers: Headers | undefined = err?.response?.headers;
if (status !== 429) throw err;
if (attempt === opts.maxRetries) break;
const retryAfterMs = headers ? parseRetryAfterMs(headers) : null;
const delayMs = retryAfterMs ?? backoffWithJitterMs(attempt, opts.baseDelayMs, opts.maxDelayMs);
await sleep(delayMs);
}
}
throw lastErr;
}
이 래퍼의 포인트는 “429면 무조건 재시도”가 아니라, 재시도 시점 제어를 헤더 기반으로 먼저 하고, 그게 없을 때만 백오프를 적용한다는 점입니다.
동시성 제어: 재시도보다 더 큰 효과
429가 자주 발생한다면, 재시도 로직만으로는 한계가 있습니다. 대부분의 경우 근본 원인은 “동시에 너무 많이 보낸다”입니다.
프로세스 내부 동시성 제한(간단하지만 효과 큼)
예: 한 프로세스에서 Claude 호출을 최대 5개까지만 동시 실행.
class Semaphore {
private available: number;
private queue: Array<() => void> = [];
constructor(max: number) {
this.available = max;
}
async acquire() {
if (this.available > 0) {
this.available -= 1;
return;
}
await new Promise<void>((resolve) => this.queue.push(resolve));
}
release() {
this.available += 1;
const next = this.queue.shift();
if (next) {
this.available -= 1;
next();
}
}
}
const sem = new Semaphore(5);
async function callClaudeWithLimit<T>(task: () => Promise<T>) {
await sem.acquire();
try {
return await task();
} finally {
sem.release();
}
}
그리고 실제 호출은 다음처럼 조합합니다.
async function run() {
return callClaudeWithLimit(() =>
withRateLimitRetry(
() => claudeRequest(),
{ maxRetries: 5, baseDelayMs: 300, maxDelayMs: 10_000 }
)
);
}
동시성 제한은 429를 줄이는 데 즉효가 있고, 재시도 트래픽의 폭발도 막아줍니다.
분산 환경(Cloud Run, K8s)에서의 함정: 인스턴스가 늘면 한도도 늘지 않는다
Cloud Run이나 Kubernetes에서 오토스케일이 걸리면 인스턴스 수가 늘어납니다. 하지만 Claude의 레이트 리밋은 보통 API 키/계정 단위로 묶이기 때문에, 인스턴스가 늘수록 429가 더 쉽게 터질 수 있습니다.
- 인스턴스 1개일 때는 괜찮았던 동시성이
- 인스턴스 10개가 되면 10배로 증가
- 외부 API 한도는 그대로라서 429 폭증
이 패턴은 504나 콜드스타트처럼 “인프라 증설로 해결”되는 문제가 아니라, 클라이언트 측에서 총량을 제어해야 해결됩니다. (Cloud Run 지연/타임아웃 운영 관점은 GCP Cloud Run 504와 콜드스타트 지연 해결 가이드도 함께 참고하면 좋습니다.)
해결책: 중앙 큐 + 워커, 또는 분산 레이트 리미터
- Pub/Sub, SQS, Redis queue 등으로 요청을 큐잉
- 워커에서 초당 처리량을 제한
- 가능하면 Redis 기반 토큰 버킷으로 “전체 클러스터”의 전역 한도를 맞춤
토큰 기반 압력 낮추기: 요청 합치기, 프롬프트 다이어트, 캐시
429는 요청 수만의 문제가 아니라 “토큰 처리량” 문제인 경우도 많습니다. 다음은 즉시 효과가 나는 실무 팁입니다.
1) 요청 배칭(가능한 경우)
- 여러 짧은 요청을 하나로 합쳐 한 번에 처리
- 결과를 분해해서 각 요청에 매핑
단, 배칭은 실패 시 영향 범위가 커지므로, 배치 크기 상한과 부분 실패 전략을 함께 설계해야 합니다.
2) 프롬프트 다이어트
- 시스템 프롬프트/정책 텍스트를 매번 크게 보내지 않도록 정리
- 불필요한 로그/원문을 그대로 붙이지 않기
- 요약 단계를 두어 “긴 입력”을 먼저 줄이고 본 호출로 넘기기
3) 응답 캐시
- 동일 입력에 대한 결과를 캐시
- 특히 “정적 지식 질의”나 “템플릿 생성”은 캐시 히트율이 높음
캐시 키는 프롬프트 문자열 전체의 해시로 잡되, 모델/버전/온도 같은 파라미터도 포함해야 합니다.
재시도 정책을 운영 친화적으로 만들기: 관측과 차단
재시도는 코드로 끝나지 않고 운영에서 완성됩니다.
지표로 꼭 봐야 할 것
429_count,429_rate- 재시도 횟수 분포(
retry_attempt_histogram) - 대기 시간(
retry_delay_ms) - 성공까지 걸린 총 시간(
time_to_success)
서킷 브레이커(폭주 방지)
429가 일정 비율 이상이면 잠시 Claude 호출을 차단하고 빠르게 실패시키는 것도 필요합니다.
- 예: 최근 1분 429 비율이 30% 초과면 30초간 오픈
- 오픈 동안은 큐에 적재하거나, 사용자에게 “잠시 후 재시도”를 안내
이 패턴은 gRPC 데드라인 전파처럼 “실패를 빠르게 전파해 전체 시스템을 보호”하는 철학과 닮아 있습니다. 관련 사고방식은 gRPC 타임아웃 지옥 탈출 - 데드라인 전파 설계도 참고할 만합니다.
TypeScript로 재시도 설정을 안전하게 관리하기
재시도 옵션은 서비스마다 조금씩 다르고, 운영 중에 값이 자주 바뀝니다. 이때 타입이 느슨하면 잘못된 설정이 그대로 배포되는 사고가 납니다.
TS에서는 satisfies로 “구조는 강제하되 리터럴 타입은 유지”하는 방식이 유용합니다.
type ClaudeRetryPolicy = {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
maxConcurrency: number;
};
export const retryPolicy = {
maxRetries: 5,
baseDelayMs: 300,
maxDelayMs: 10_000,
maxConcurrency: 5,
} satisfies ClaudeRetryPolicy;
이 패턴은 설정 객체가 커질수록 효과가 커집니다. 더 자세한 타입 안정화는 TS 5.x satisfies로 타입 오류 줄이는 실전에서 확장해볼 수 있습니다.
실무 체크리스트
마지막으로 429 대응을 배포 전에 점검할 체크리스트를 정리합니다.
- 429에서
retry-after를 우선 존중하는가 - 지수 백오프에 지터가 포함되어 있는가
- 최대 재시도 횟수/최대 대기 상한이 있는가
- 프로세스 내부 동시성 제한이 있는가
- 오토스케일 환경에서 “전역 한도”를 고려했는가
- 429 비율이 일정 수준을 넘으면 서킷 브레이커 또는 큐잉으로 전환하는가
- 429/재시도 관련 지표가 대시보드에 있는가
결론: 429는 재시도 문제가 아니라 “흐름 제어” 문제다
Claude 429를 안정적으로 다루려면, 재시도는 최소 조건일 뿐이고 흐름 제어(flow control) 가 핵심입니다. 헤더 기반 대기, 지터 백오프, 동시성 제한만 제대로 적용해도 429 폭증의 대부분은 잡힙니다. 그 다음 단계로는 전역 레이트 리미팅, 큐잉, 프롬프트/토큰 최적화, 캐시를 통해 “요청 자체의 압력”을 낮추는 방향으로 가야 운영이 편해집니다.
운영 중 429가 늘기 시작했다면, 재시도 횟수를 늘리기 전에 먼저 “동시에 몇 개를 보내고 있는지”, “토큰을 얼마나 쓰고 있는지”, “재시도들이 같은 타이밍에 몰리고 있지 않은지”부터 확인해보는 것을 권합니다.