Advanced (업데이트: 2026. 6. 2.)

Claude Code로 API Rate Limiting 구현하기: 429, Redis, Cloudflare 실전

Claude Code로 API rate limiting을 안전하게 구현합니다. 429, Redis, Cloudflare, abuse 방지와 실패 사례까지 다룹니다.

Claude Code로 API Rate Limiting 구현하기: 429, Redis, Cloudflare 실전

API rate limiting, 즉 API 요청 제한은 같은 사용자나 스크립트가 짧은 시간 안에 보낼 수 있는 요청 수를 정하고, 초과하면 잠시 기다리게 하는 장치입니다. 서비스를 모두 막는 것이 아니라, 한 클라이언트가 과도하게 자원을 쓰지 못하게 조절합니다. 인기 매장에서 번호표를 나눠 주는 것과 비슷합니다.

Claude Code는 endpoint, 인증, 테스트를 빠르게 만들어 줍니다. 하지만 동작하는 API가 곧 운영 가능한 API는 아닙니다. 로그인 시도, 검색 API, AI 생성, SMS 인증, 이메일 발송, webhook 재시도는 모두 비용과 부하를 만듭니다. Masa가 작은 문의 폼을 검증할 때도 중복 제출을 단순한 UX 문제로 봤다가, QA 중에 이메일 발송 한도가 빠르게 줄었습니다. 문제는 폼 하나가 아니라 “이 행동은 얼마나 자주 허용되는가”를 설계하지 않은 데 있었습니다.

이 글은 Claude Code에 rate limiting을 맡길 때 필요한 설계 메모, 의존성 없는 Node.js 예제, Redis 기반 Express 구현, 클라이언트 재시도 코드, Cloudflare 배치, 보안 관점과 실패 사례를 한 번에 정리합니다. API 설계의 큰 흐름은 Claude Code production API development, 안전한 사용은 Claude Code security best practices, edge 쪽 기초는 Cloudflare Workers guide를 함께 보면 좋습니다.

공식 기준은 Cloudflare Rate limiting rules, OWASP API Security 2023의 API4: Unrestricted Resource ConsumptionAPI6: Unrestricted Access to Sensitive Business Flows, 그리고 MDN의 429 Too Many Requests를 참고합니다.

무엇을 보호할지 먼저 정하기

초보자는 보통 “분당 60회”처럼 숫자부터 정합니다. 더 나은 순서는 위험을 먼저 나누는 것입니다. rate limiting은 서버만 보호하지 않습니다. DB 부하, 외부 API 비용, 재고, 비밀번호 재설정 흐름, 이메일 발송량, AI credit, lead 품질, 비즈니스 규칙을 함께 보호합니다.

flowchart LR
  A["Request"] --> B["Identify client"]
  B --> C["Check policy"]
  C -->|allowed| D["Run handler"]
  C -->|too many| E["Return 429 + Retry-After"]
  D --> F["Log count and cost"]

현실적인 시작점은 다음과 같습니다.

Use case제한 key시작 기준보호 대상
로그인, OTP, 비밀번호 재설정IP + account id10분 5회brute force, SMS 비용
검색/목록 APIuser id + path분당 60회DB 부하, scraping
AI/이미지 생성user id + plan무료 plan 하루 10회LLM 비용, 무료 한도
Webhook 수신sender + event id짧은 burst 허용중복 처리, queue

IP만 믿으면 안 됩니다. 회사, 학교, 모바일 네트워크에서는 많은 정상 사용자가 같은 IP로 보일 수 있고, 공격자는 IP를 쉽게 바꿉니다. 인증된 API라면 user id, API key, organization id, plan, endpoint, operation type을 조합해서 key를 만드는 편이 안전합니다.

Claude Code에는 명확한 사양을 준다

“rate limiting을 추가해줘”는 너무 넓습니다. 알고리즘, key 전략, 429 응답, header, 테스트, 로그, local과 production 저장소를 명시해야 합니다. 아래 prompt는 그대로 붙여 넣고 시작할 수 있습니다.

Add rate limiting to the existing API.

Requirements:
- Scope: POST /api/contact and POST /api/login
- If authenticated, key by userId; otherwise key by IP
- 429 JSON body: { "error": "rate_limited", "retryAfter": seconds }
- Return Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining
- Tests must cover allowed requests, limit reached, and recovery after time passes
- Use Redis in production and an in-memory store locally
- Make limits configurable through environment variables

After implementation, report the verification commands and any unverified risks.

이렇게 쓰면 완료 조건이 분명해집니다. Claude Code가 middleware만 추가하고 client contract를 놓치는 일을 줄일 수 있습니다. API testing automation guide처럼 성공 응답뿐 아니라 429 응답도 테스트로 고정하세요.

바로 실행하는 최소 예제: Node.js 429 서버

아래 코드를 rate-limit-demo.mjs로 저장하고 Node.js 20 이상에서 실행합니다. token bucket은 bucket에 token이 일정 속도로 채워지고, 요청마다 1개를 소비하는 방식입니다. 짧은 burst는 허용하면서 장기 평균 속도를 제한할 수 있습니다.

import http from "node:http";

class TokenBucket {
  constructor({ capacity, refillPerSecond }) {
    this.capacity = capacity;
    this.refillPerSecond = refillPerSecond;
    this.tokens = capacity;
    this.updatedAt = Date.now();
  }

  take(now = Date.now()) {
    const elapsed = (now - this.updatedAt) / 1000;
    this.tokens = Math.min(
      this.capacity,
      this.tokens + elapsed * this.refillPerSecond,
    );
    this.updatedAt = now;

    if (this.tokens >= 1) {
      this.tokens -= 1;
      return { allowed: true, remaining: Math.floor(this.tokens), retryAfter: 0 };
    }

    const missing = 1 - this.tokens;
    const retryAfter = Math.ceil(missing / this.refillPerSecond);
    return { allowed: false, remaining: 0, retryAfter };
  }
}

const buckets = new Map();

function clientKey(req) {
  return req.headers["x-api-key"] ?? req.socket.remoteAddress ?? "anonymous";
}

function checkLimit(req) {
  const key = clientKey(req);
  if (!buckets.has(key)) {
    buckets.set(key, new TokenBucket({ capacity: 5, refillPerSecond: 1 }));
  }
  return buckets.get(key).take();
}

const server = http.createServer((req, res) => {
  if (req.url !== "/api/demo") {
    res.writeHead(404, { "content-type": "application/json" });
    res.end(JSON.stringify({ error: "not_found" }));
    return;
  }

  const result = checkLimit(req);
  res.setHeader("X-RateLimit-Limit", "5");
  res.setHeader("X-RateLimit-Remaining", String(result.remaining));

  if (!result.allowed) {
    res.writeHead(429, {
      "content-type": "application/json",
      "Retry-After": String(result.retryAfter),
    });
    res.end(JSON.stringify({
      error: "rate_limited",
      retryAfter: result.retryAfter,
    }));
    return;
  }

  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ ok: true, remaining: result.remaining }));
});

server.listen(3000, () => {
  console.log("Listening on http://localhost:3000/api/demo");
});
node rate-limit-demo.mjs

다른 터미널에서 연속 호출합니다.

for i in 1 2 3 4 5 6 7; do
  curl -i http://localhost:3000/api/demo
done

Windows PowerShell에서는 다음처럼 확인할 수 있습니다.

1..7 | ForEach-Object {
  curl.exe -i http://localhost:3000/api/demo
}

6번째나 7번째 요청에서 429 Too Many Requests가 나오면 성공입니다. MDN이 설명하듯 429에는 Retry-After를 붙일 수 있고, 이 header가 있어야 client가 무작정 재시도하지 않습니다.

Redis로 여러 서버에 대응하기

메모리 구현은 학습에는 좋지만 API 서버가 여러 대가 되면 깨집니다. A 서버는 남은 횟수가 0이라고 보고, B 서버는 아직 5회 남았다고 볼 수 있습니다. Redis에 카운트를 모으면 모든 인스턴스가 같은 상태를 봅니다.

아래 Express 예제는 Redis sorted set으로 sliding window를 만듭니다. sliding window는 “현재 시점에서 직전 60초”를 계속 움직이며 세기 때문에, 매분 0초에 한꺼번에 초기화되는 fixed window보다 자연스럽습니다.

npm init -y
npm i express ioredis
docker run --rm --name redis-rate-limit -p 6379:6379 redis:7-alpine
import express from "express";
import Redis from "ioredis";

const app = express();
const redis = new Redis(process.env.REDIS_URL ?? "redis://127.0.0.1:6379");

const limitScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local member = ARGV[4]

redis.call("ZREMRANGEBYSCORE", key, 0, now - window_ms)

local count = redis.call("ZCARD", key)
if count >= limit then
  local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")[2]
  local retry_ms = math.max(1, oldest + window_ms - now)
  return {0, 0, retry_ms}
end

redis.call("ZADD", key, now, member)
redis.call("PEXPIRE", key, window_ms)
return {1, limit - count - 1, 0}
`;

async function rateLimit(req, res, next) {
  const user = req.get("authorization")?.replace(/^Bearer\s+/i, "");
  const identity = user || req.ip || "anonymous";
  const key = `rl:${identity}:${req.path}`;
  const limit = Number(process.env.RATE_LIMIT_REQUESTS ?? 10);
  const windowMs = Number(process.env.RATE_LIMIT_WINDOW_MS ?? 60000);
  const now = Date.now();
  const member = `${now}:${Math.random()}`;

  const [allowed, remaining, retryMs] = await redis.eval(
    limitScript,
    1,
    key,
    limit,
    windowMs,
    now,
    member,
  );

  res.setHeader("X-RateLimit-Limit", String(limit));
  res.setHeader("X-RateLimit-Remaining", String(remaining));

  if (allowed === 1) return next();

  const retryAfter = Math.ceil(Number(retryMs) / 1000);
  res.setHeader("Retry-After", String(retryAfter));
  res.status(429).json({ error: "rate_limited", retryAfter });
}

app.use(rateLimit);

app.get("/api/search", (req, res) => {
  res.json({ data: ["claude-code", "rate-limit"], at: new Date().toISOString() });
});

app.listen(3000, () => {
  console.log("API ready on http://localhost:3000/api/search");
});
node redis-rate-limit-server.mjs
for i in $(seq 1 12); do
  curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/search
done

Claude Code에 production 구현을 맡길 때는 Redis 장애 시 동작도 적어야 합니다. 문의 폼은 잠깐 fail open이 괜찮을 수 있지만, 로그인, 결제, AI credit endpoint는 fail closed가 맞을 수 있습니다. 이 판단은 library가 아니라 비즈니스 리스크로 정해야 합니다.

클라이언트도 Retry-After를 지켜야 한다

서버가 429를 반환해도 SDK나 batch가 즉시 다시 보내면 효과가 줄어듭니다. Retry-After를 읽고 기다린 뒤 재시도하세요.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchWithRateLimit(url, options = {}, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;

    const retryAfter = Number(res.headers.get("retry-after") ?? "1");
    const waitMs = Math.max(1, retryAfter) * 1000;
    console.log(`429 received. Waiting ${waitMs}ms before retry.`);
    await sleep(waitMs);
  }

  throw new Error("Rate limit retry budget exhausted");
}

for (let i = 0; i < 8; i += 1) {
  const res = await fetchWithRateLimit("http://localhost:3000/api/demo");
  console.log(i + 1, res.status, await res.text());
}

외부 API client를 Claude Code로 만들 때는 “429이면 exponential backoff, Retry-After가 있으면 우선 사용, 최대 재시도 횟수 제한, 최종 실패 로그 기록”이라고 지정하세요. 무한 재시도는 장애를 키웁니다.

Cloudflare는 입구, 앱은 사용자 규칙

Cloudflare Rate Limiting Rules는 origin에 닿기 전에 명백한 폭주 트래픽을 막는 데 좋습니다. 공식 문서는 expression, period, request threshold, mitigation timeout, action을 설명합니다. 로그인 페이지, 공개 검색 API, admin route, AI 생성 진입점, bot pattern에 잘 맞습니다.

하지만 Cloudflare만으로 product limit을 처리할 수는 없습니다. 무료/유료 plan, 조직 사용량, 사용자별 AI credit, 환불 악용, 초대 보상 악용은 application data가 필요합니다. 실무에서는 다음처럼 나눕니다.

Layer역할예시
Cloudflare/WAF명백한 burst와 bot을 먼저 차단/api/login을 IP별 제한
Applicationuser, organization, plan, operation 기준 제한무료 사용자는 하루 10회 생성
Queue/worker무거운 비동기 작업 평준화email, image, PDF 작업
Billing/monitoring비용 이상 감지SMS와 LLM 사용량 알림

OWASP API4는 CPU, memory, file size, third-party service 사용량에 제한이 없을 때 생기는 위험을 다룹니다. OWASP API6는 구매, 예약, posting, referral 같은 민감한 business flow가 자동화로 남용되는 문제를 다룹니다. 즉 rate limiting은 DDoS 방어만이 아니라 무료 한도 소진, 재판매, spam, SMS 비용 폭증, 계정 공격을 막는 수익 방어 장치입니다.

흔한 실패 사례

첫 번째 실패는 모든 API에 같은 제한을 거는 것입니다. profile read와 password reset은 같은 threshold를 쓰면 안 됩니다. operation별 비용과 위험으로 나누세요.

두 번째는 429 응답 형식이 제각각인 것입니다. 어떤 route는 HTML, 어떤 route는 plain text, 어떤 route는 JSON이면 client가 불안정해집니다. JSON body, Retry-After, remaining header를 통일하세요.

세 번째는 성공 요청만 세는 것입니다. 로그인 실패, invalid payload, 존재하지 않는 이메일의 password reset도 비용과 공격 신호를 가집니다. 실패 요청을 더 강하게 제한해야 하는 경우가 많습니다.

네 번째는 개인 정보를 key에 그대로 넣는 것입니다. 이메일이나 전화번호를 Redis key와 log에 평문으로 남기지 마세요. 필요하면 hash하고 TTL을 짧게 둡니다.

다섯 번째는 테스트에서 실제로 60초를 기다리는 것입니다. CI가 느려집니다. limiter가 now를 주입받도록 만들고 테스트에서는 시간을 직접 이동하세요.

마지막은 정상 인프라를 막는 것입니다. 검색 bot, uptime check, 내부 monitoring, payment webhook, partner callback은 별도 정책이 필요할 수 있습니다. 예외는 좁고 기록 가능해야 합니다.

Claude Code 리뷰 체크리스트

구현 후 Claude Code에 아래 항목으로 다시 리뷰하게 하세요.

  • 모든 429가 같은 JSON shape인지
  • Retry-After와 remaining-count header가 있는지
  • IP, user id, API key, organization id key 전략이 맞는지
  • Redis 장애 시 fail open/closed가 명시됐는지
  • 인증 실패, validation 실패, 외부 API 실패를 필요한 만큼 세는지
  • allowed, blocked, recovered 상태 테스트가 있는지
  • admin, monitoring, webhook, crawler 예외가 너무 넓지 않은지

이는 단순 code quality가 아닙니다. AI, SMS, email, payment가 붙은 제품에서는 limit 누락이 바로 비용으로 나타납니다.

상담 CTA

ClaudeCodeLab의 Claude Code training and consulting에서는 API 구현, security review, rate limiting, billing safeguard, monitoring 설계를 함께 다룹니다. 기존 Next.js, Express, Cloudflare Workers, AWS API Gateway 구성을 기준으로 “어떤 행동을, 누구에게, 얼마나 자주 허용할지”를 code, test, log로 떨어뜨리는 것이 핵심입니다.

개인 프로젝트는 먼저 이 글의 Node.js demo를 실행하고, 인스턴스가 둘 이상이 되는 시점에 Redis로 옮기면 충분합니다. 팀에서는 Claude Code prompt, review checklist, environment variable, runbook까지 같이 관리해야 나중에 threshold를 바꿔도 흔들리지 않습니다.

이 글의 예제를 직접 확인한 결과, memory server는 반복 요청 후 429를 반환했고 Redis 버전은 10회 window를 넘은 뒤 Retry-After가 있는 429를 반환했습니다. client wait logic을 넣자 즉시 재시도가 멈췄습니다. 결론은 단순합니다. rate limit은 middleware 하나가 아니라 response, retry, log, exception 정책까지 함께 검증해야 production-ready입니다.

#Claude Code #rate limiting #API #security #Node.js
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.