Published on

Claude API 429 과금·레이트리밋 해결 가이드

Authors

Claude API를 붙여 개발하다 보면 어느 순간 429가 튀어나오며 요청이 거절됩니다. 문제는 429가 단순히 "너무 많이 호출했다"만 의미하지 않는다는 점입니다. 실제 운영에서는 과금 한도(credit/limit) 문제레이트리밋(RPM/TPM 등) 초과가 모두 429로 나타날 수 있고, 둘은 해결책이 완전히 다릅니다.

이 글에서는 Claude API의 429원인별로 분해해 진단하는 방법, 그리고 재시도(backoff)만으로는 해결되지 않는 케이스(동시성 폭주, 토큰 폭주, 배치/스트리밍 설계 미스)를 안정적으로 해결하는 패턴을 정리합니다.

운영 트러블슈팅 관점에서의 접근법은 다른 장애 분석에도 그대로 적용됩니다. 예를 들어 인증 계열 문제를 시간 오차로 좁혀가는 방식은 Nginx에서 JWT 401 간헐 발생 - 시계오차 해결 같은 글과 결이 비슷합니다.

429의 두 얼굴: 과금(결제/크레딧) vs 레이트리밋

429를 보면 먼저 아래 두 축으로 나눠야 합니다.

1) 과금·크레딧·하드 리밋 이슈

다음 상황이면 호출량이 많지 않아도 429가 날 수 있습니다.

  • 무료 크레딧 소진, 혹은 결제 수단/플랜 이슈로 청구가 막힘
  • 월간/일간 예산(cap) 설정에 걸림
  • 조직 단위의 결제 한도 또는 승인 문제

이 경우는 재시도 로직으로 절대 해결되지 않습니다. 결제/플랜/예산을 조정해야 합니다.

2) 레이트리밋 이슈(RPM/TPM/동시성)

레이트리밋은 보통 다음 중 하나로 터집니다.

  • RPM(requests per minute) 초과: 짧은 시간에 요청 수 폭주
  • TPM(tokens per minute) 초과: 요청 수는 적어도 토큰이 큰 요청이 몰림
  • 동시성 제한 초과: 병렬 요청이 제한을 넘음

이 경우는 요청을 줄이거나, 분산시키거나, 토큰을 줄이거나, 큐잉/스로틀링 해야 합니다.

먼저 확인할 것: 응답 바디와 헤더를 반드시 로깅

429를 제대로 분류하려면, 애플리케이션 로그에 아래를 남겨야 합니다.

  • HTTP status
  • response body(에러 타입/메시지)
  • rate limit 관련 헤더(있다면)
  • 요청의 메타: 모델, max_tokens, 입력 길이(대략), 스트리밍 여부

특히 response body에 "insufficient credit" 류가 있으면 과금 문제일 가능성이 큽니다.

Node.js(axios)에서 429 로깅 예시

import axios from "axios";

async function callClaude(payload: any) {
  try {
    const res = await axios.post(
      "https://api.anthropic.com/v1/messages",
      payload,
      {
        headers: {
          "x-api-key": process.env.ANTHROPIC_API_KEY!,
          "anthropic-version": "2023-06-01",
          "content-type": "application/json",
        },
        timeout: 30_000,
      }
    );

    return res.data;
  } catch (err: any) {
    const status = err?.response?.status;
    const data = err?.response?.data;
    const headers = err?.response?.headers;

    console.error("Claude error", {
      status,
      data,
      rateLimit: {
        // 제공되는 경우만 존재
        limit: headers?.["x-ratelimit-limit"],
        remaining: headers?.["x-ratelimit-remaining"],
        reset: headers?.["x-ratelimit-reset"],
      },
    });

    throw err;
  }
}

주의할 점은, 헤더 키는 공급자/버전에 따라 다를 수 있습니다. 따라서 있으면 기록하고, 없으면 body 중심으로 분류하는 쪽이 안전합니다.

과금(결제/크레딧) 문제일 때의 해결 체크리스트

과금 이슈는 기술적으로 우회할 방법이 없습니다. 다만 운영에서 자주 놓치는 지점들이 있습니다.

1) 조직(Organization)과 API 키 소속 확인

  • API 키가 어떤 조직/프로젝트에 속해 있는지
  • 결제 수단이 연결된 주체가 동일한지
  • 샌드박스/개발 조직 키를 운영에서 쓰고 있지 않은지

멀티 조직을 쓰면 "내 계정엔 결제가 되어 있는데 운영은 429" 같은 상황이 흔합니다.

2) 예산(cap)과 알림 설정

  • 월간 예산이 너무 낮게 잡혀서 조기 차단
  • 알림이 없어서 소진을 뒤늦게 인지

운영에서는 예산 상한 + 알림을 함께 설정하고, 애플리케이션에서도 결제성 429를 감지하면 즉시 알림을 보내는 것이 좋습니다.

3) 장애 모드 설계(Graceful degradation)

과금 429는 재시도해도 안 풀리므로, 아래 중 하나를 선택해야 합니다.

  • 기능을 제한 모드로 전환(예: 요약만 제공)
  • 더 저렴한 모델/짧은 출력으로 강등
  • 캐시된 결과 제공
  • "잠시 후" 안내와 함께 비동기 처리로 전환

레이트리밋 문제일 때: 재시도만으로는 부족하다

레이트리밋은 보통 "순간 폭주" 때문에 발생합니다. 이때 단순 재시도는 오히려 트래픽을 더 몰아넣어 악화시킬 수 있습니다.

해결은 크게 4가지 축입니다.

  1. 재시도(backoff)와 지터(jitter)
  2. 클라이언트 측 스로틀링(토큰 버킷/리키 버킷)
  3. 큐잉(작업 대기열)과 워커 동시성 제한
  4. 토큰 예산 관리(입력/출력 토큰 줄이기)

1) 올바른 재시도: 지터 포함 지수 백오프

429는 "잠깐 기다리면 풀리는" 경우가 많지만, 동시에 다수 인스턴스가 재시도하면 다시 폭주합니다. 그래서 지터가 핵심입니다.

Node.js 재시도 유틸 예시

function sleep(ms: number) {
  return new Promise((r) => setTimeout(r, ms));
}

function backoffDelayMs(attempt: number) {
  const base = 300; // 0.3s
  const cap = 10_000; // 10s
  const exp = Math.min(cap, base * 2 ** attempt);
  const jitter = Math.floor(Math.random() * 250);
  return exp + jitter;
}

async function withRetry<T>(fn: () => Promise<T>) {
  const maxAttempts = 6;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (e: any) {
      const status = e?.response?.status;
      if (status !== 429) throw e;

      const delay = backoffDelayMs(attempt);
      await sleep(delay);
      continue;
    }
  }

  throw new Error("Claude 429: retries exhausted");
}

실전 팁:

  • Retry-After 헤더가 있으면 그 값을 우선
  • 재시도는 idempotent한 요청에만 안전합니다. 채팅 생성은 멱등이 아니므로, 클라이언트 요청 ID를 두고 중복 응답을 제거하는 장치가 필요합니다.

2) 동시성 제한: 서버 내부에서 먼저 막아라

운영에서 가장 흔한 패턴은 다음입니다.

  • 사용자 요청이 몰림
  • 서버가 요청을 그대로 Claude로 팬아웃
  • 순간적으로 동시 호출이 수십~수백으로 튐
  • 429 연쇄 발생

이때는 서버 내부에서 Claude 호출 동시성을 제한해야 합니다.

간단한 세마포어로 동시성 제한(TypeScript)

class Semaphore {
  private queue: Array<() => void> = [];
  private count: number;

  constructor(private readonly capacity: number) {
    this.count = capacity;
  }

  async acquire() {
    if (this.count > 0) {
      this.count -= 1;
      return;
    }
    await new Promise<void>((resolve) => this.queue.push(resolve));
  }

  release() {
    this.count += 1;
    const next = this.queue.shift();
    if (next && this.count > 0) {
      this.count -= 1;
      next();
    }
  }
}

const claudeSemaphore = new Semaphore(5); // 동시 호출 5개로 제한

async function callClaudeWithConcurrencyLimit(payload: any) {
  await claudeSemaphore.acquire();
  try {
    return await withRetry(() => callClaude(payload));
  } finally {
    claudeSemaphore.release();
  }
}

이 방식은 단일 프로세스에서는 효과적이지만, 서버가 여러 대면 인스턴스별로 5개씩 열려 총량이 커집니다. 그 경우 Redis 기반 분산 세마포어나 중앙 큐를 고려하세요.

3) 큐잉으로 구조를 바꾸면 429가 사라진다

사용자 요청을 동기적으로 처리하면, 프론트 트래픽 패턴이 곧바로 LLM 호출 패턴이 됩니다. 429를 근본적으로 줄이려면 다음 구조가 안정적입니다.

  • API 서버는 요청을 받고 작업을 큐에 넣음
  • 워커가 제한된 동시성으로 Claude 호출
  • 결과를 저장하고 폴링/웹훅/소켓으로 전달

이 패턴은 쿠버네티스에서도 동일하게 적용됩니다. 워커 스케일링을 잘못하면 egress나 LB 계층에서 다른 증상으로 나타나기도 하니, 네트워크 레벨 추적이 필요하면 EKS에서 Pod egress만 502? Envoy/NLB 추적기 같은 접근을 참고할 만합니다.

4) TPM(토큰/분) 초과를 줄이는 토큰 예산 관리

요청 수는 많지 않은데도 429가 난다면, 범인은 대개 TPM입니다. 즉, 한 번 호출할 때 토큰을 너무 많이 쓰고 있습니다.

TPM을 폭발시키는 흔한 실수

  • 시스템 프롬프트에 장문의 정책/문서를 매번 통째로 붙임
  • 대화 히스토리를 무제한으로 누적
  • max_tokens를 과하게 크게 설정
  • JSON 출력 강제 + 스키마 설명을 매번 장황하게 포함

해결 전략

  • 히스토리는 요약본으로 롤업(예: 10턴마다 요약)
  • 문서는 RAG로 필요한 부분만 발췌
  • max_tokens를 제품 요구사항에 맞게 보수적으로
  • 응답 포맷을 간결화(필요한 필드만)

간단한 토큰 예산 가드 예시

정확한 토큰 카운트는 모델/토크나이저에 따라 다르지만, 운영 가드로는 "대략적인 문자 수" 기반 제한도 효과가 있습니다.

function approxTokenCount(text: string) {
  // 매우 러프한 근사: 영어 4자당 1토큰, 한글은 더 촘촘할 수 있음
  return Math.ceil(text.length / 4);
}

function enforceBudget(input: string, maxInputTokens: number) {
  const t = approxTokenCount(input);
  if (t > maxInputTokens) {
    throw new Error(`Input too large: ${t} tokens (budget ${maxInputTokens})`);
  }
}

핵심은 "정확한 토큰 계산"보다도, 폭주를 사전에 차단해 TPM 기반 429를 줄이는 것입니다.

스트리밍을 쓰면 429가 줄어드나?

스트리밍은 사용자 체감 지연을 줄이는 데는 좋지만, 레이트리밋 자체를 마법처럼 해결하진 않습니다. 다만 다음 효과는 있습니다.

  • 서버 타임아웃/재시도 중복을 줄여 간접적으로 429를 완화
  • 긴 응답에서 사용자가 중간에 취소하면 토큰 사용량을 줄일 여지

취소 처리가 없다면 오히려 연결이 오래 유지되어 동시성 관점에서 불리할 수도 있습니다.

운영 관측: 429를 "측정 가능한" 문제로 만들기

레이트리밋은 감으로 고치면 재발합니다. 아래 지표를 최소로 잡으세요.

  • 분당 요청 수(RPM)
  • 분당 토큰 사용량 추정(TPM)
  • 동시 처리 중인 Claude 호출 수(in-flight)
  • 429 비율, 429 발생 시점의 동시성/토큰
  • 재시도 횟수 분포(평균이 아니라 p95, p99)

그리고 알림은 두 갈래로 나누는 것이 좋습니다.

  • 과금성 429 감지 알림: 즉시(재시도 무의미)
  • 레이트리밋 429 알림: 임계치 초과 시(예: 5분 평균 1% 이상)

실전 대응 시나리오: "갑자기 429가 늘었다"

  1. 같은 시간대에 배포/트래픽 변화가 있었는지 확인
  2. 429 응답 body로 과금 vs 레이트리밋 분류
  3. 레이트리밋이면 RPM/TPM/동시성 중 무엇이 병목인지 로그로 확인
  4. 즉시 완화
    • 동시성 제한 수치를 내림
    • max_tokens를 낮춤
    • 재시도에 지터 추가
  5. 근본 개선
    • 큐 기반 비동기화
    • 히스토리 요약/문서 발췌로 토큰 절감
    • 캐시(같은 입력에 같은 출력이 허용되는 업무라면)

이 과정은 "원인 후보를 좁혀가며 재현 가능한 증거를 쌓는" 방식이 핵심입니다. 런타임 리소스 문제를 로그로 특정하는 접근은 리눅스 OOM Killer 로그로 원인 프로세스 찾기 같은 글과도 통합니다.

마무리: 429를 없애는 가장 현실적인 조합

  • 과금 429: 결제/예산/조직 소속부터 확인하고, 앱은 제한 모드로 전환
  • 레이트리밋 429: 지터 백오프 + 동시성 제한 + 큐잉 + 토큰 예산을 조합
  • 관측: 429를 "에러"가 아니라 "용량 신호"로 보고 지표화

이렇게 설계하면 429는 "가끔 뜨는 불쾌한 에러"가 아니라, 시스템이 감당 가능한 처리량을 알려주는 피드백으로 바뀝니다. 운영이 가능한 형태로 Claude API를 붙이는 데 가장 중요한 관점입니다.