Claude CodeでAPIレート制限を実装する実践ガイド | 429・Redis・Cloudflare対応
Claude CodeでAPIレート制限を安全に実装。429、Redis、Cloudflare、失敗例まで実践解説。
APIのレート制限とは、同じ利用者が短時間に送れるリクエスト数を決めて、超えたら待ってもらう仕組みです。店の入り口で整理券を配るイメージに近いです。全員を止めるのではなく、同じ人が連打したときだけ「少し待ってください」と返します。
Claude CodeでAPIを作ると、エンドポイント、認証、テストまでは素早く形になります。ところがレート制限を後回しにすると、ログイン試行、検索API、AI呼び出し、メール送信、Webhookの再送で、想定より早くコストと負荷が跳ねます。Masaが小さな問い合わせAPIを試作したときも、フォームの二重送信を軽く見たせいで、検証中にメール送信枠を数百件消費しました。バグではなく、制限を設計していなかったことが原因でした。
この記事では、Claude Codeに丸投げしないための設計メモ、コピペで動くNode.js例、Redisを使う実務寄りの実装、Cloudflareで入口を守る考え方までまとめます。API全体の設計はClaude Codeで本番API開発、安全運用はClaude Codeセキュリティベストプラクティス、Cloudflare Workers側の基礎はClaude Code Cloudflare Workersも合わせて確認してください。
公式情報は、CloudflareのRate limiting rules、OWASP API Security 2023のAPI4: Unrestricted Resource ConsumptionとAPI6: Unrestricted Access to Sensitive Business Flows、HTTP 429の意味を説明するMDN 429 Too Many Requestsを基準にします。
まず「何を守るか」を決める
初心者が最初に迷うのは、「1分に何回まで」と数字から考えてしまうことです。数字の前に、守る対象を分けます。レート制限は単なる速度調整ではなく、サーバー、外部API費用、在庫、メール送信枠、ログイン画面、営業リードの品質を守るための制御です。
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"]
実務で多いユースケースは次の4つです。
| ユースケース | 制限の軸 | 目安 | 守りたいもの |
|---|---|---|---|
| ログイン・OTP・パスワードリセット | IP + アカウントID | 5回/10分 | 総当たり、SMS費用 |
| 検索・一覧API | ユーザーID + path | 60回/分 | DB負荷、スクレイピング |
| AI生成・画像生成API | ユーザーID + プラン | 10回/日から | API料金、無料枠 |
| Webhook受信 | 送信元 + event id | 短い再送は許可 | 二重処理、キュー詰まり |
ここで大事なのは、IPだけに頼らないことです。会社や学校のNAT環境では多人数が同じIPに見えます。逆に攻撃者はIPを簡単に変えます。認証済みAPIならユーザーID、APIキー、組織ID、プラン、エンドポイントを組み合わせてキーを作るほうが現実的です。
Claude Codeに渡す仕様を先に書く
Claude Codeへの依頼は「レート制限を実装して」では粗すぎます。アルゴリズム、キー、429レスポンス、ヘッダー、テスト、ログを指定します。以下のプロンプトをそのまま貼ると、レビューしやすい差分になります。
既存のAPIにレート制限を追加してください。
要件:
- 対象は POST /api/contact と POST /api/login
- 認証済みなら userId、未認証なら IP をキーにする
- 429 のJSONは { "error": "rate_limited", "retryAfter": 秒数 }
- Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining を返す
- テストでは許可、上限到達、時間経過後の復帰を確認する
- 本番ではRedis、ローカルではメモリ実装で動く構成にする
- 制限値は環境変数で変えられるようにする
実装後に、実行した確認コマンドと未確認リスクを短く報告してください。
この書き方にすると、Claude Codeは「どこまでが成功か」を理解できます。さらにAPIテスト自動化ガイドのように、成功時だけでなく失敗時のレスポンスも固定しておくと、あとでフロントエンドやSDKが扱いやすくなります。
コピペで動く最小実装: Node.jsだけで429を返す
まずは依存なしで動く例です。rate-limit-demo.mjsとして保存し、Node.js 20以上で実行してください。トークンバケットは、バケツに一定速度でチケットが補充され、リクエストごとに1枚使う方式です。短いバーストを許しつつ、長時間の平均速度を抑えられます。
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回目以降で429 Too Many Requestsが返れば成功です。MDNが説明している通り、429では「いつ再試行できるか」をRetry-Afterで返すと、クライアント側が無駄な再送を避けられます。
Redisで複数台構成に耐える実装にする
メモリ実装は学習には便利ですが、本番でサーバーが2台以上あると破綻します。Aサーバーでは残り0、Bサーバーでは残り5のようにカウントが分かれるからです。そこでRedisにカウントを寄せます。
次の例は、ExpressとRedis Sorted Setを使ったスライディングウィンドウです。スライディングウィンドウは「直近60秒」のように動く時間枠で数える方式で、毎分0秒にカウントが一斉リセットされる固定窓よりも自然です。
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
この実装は、APIサーバーを複数台に増やしても同じRedisを見ます。Claude Codeに本番実装を頼むときは、Redis障害時に「全許可に倒すか、全拒否に倒すか」も明記してください。問い合わせフォームなら一時的に許可、ログインや決済なら厳しく拒否、のように業務リスクで分けます。
クライアントはRetry-Afterを尊重する
サーバーが429を返しても、クライアントが即再送を続けると意味がありません。SDK、バッチ、Webhook送信側では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());
}
これはバッチ処理で特に効きます。Claude Codeに外部API連携を書かせるときは、「429なら指数バックオフ、Retry-Afterがあれば優先、最大再試行回数を超えたら失敗として記録」と指定してください。無限リトライは、障害時に相手先と自分の両方を壊します。
Cloudflareで入口、アプリでユーザー単位を守る
CloudflareのRate Limiting Rulesは、エッジで大量アクセスを早く落とすのに向いています。公式ドキュメントにも、式、期間、リクエスト数、緩和時間、アクションなどのパラメータが説明されています。ログイン画面、検索API、WordPressやAstroの管理画面、AI生成APIの入口に置くと、アプリまで届く前の負荷を減らせます。
ただしCloudflareだけで完結させるのは危険です。無料プランと有料プラン、組織ごとの上限、ユーザーID単位のAI生成回数、返金濫用の検出は、アプリ側のデータを見ないと判断できません。実務では次の二段構えにします。
| 層 | 役割 | 例 |
|---|---|---|
| Cloudflare/WAF | 明らかな連打、bot、国・ASN単位の異常を止める | /api/loginをIPごとに制限 |
| アプリ | ユーザー、組織、プラン、操作種別で制限 | 無料ユーザーはAI生成10回/日 |
| キュー/ワーカー | 重い処理を平準化する | メール送信、画像生成、PDF作成 |
| 請求・監視 | 異常費用を検知する | SMS費用、LLM API料金のアラート |
OWASP API SecurityのAPI4は、CPU、メモリ、ファイルサイズ、外部サービス費用などの無制限な消費を問題にしています。API6は、購入、予約、招待、投稿のようなビジネス上重要な流れを自動化される危険を扱います。つまりレート制限はDDoS対策だけではありません。不正利用、転売、無料枠の食い潰し、SMS費用の爆発を防ぐ収益防衛策でもあります。
失敗例と落とし穴
よくある失敗は、まず「全APIを同じ上限」にすることです。プロフィール取得とパスワードリセットを同じ60回/分にすると、片方は厳しすぎ、もう片方は緩すぎます。エンドポイントごとにコストとリスクを見ます。
次に、429のレスポンス形式を決めないことです。HTMLのエラーページを返すと、フロントエンドもSDKも扱いに困ります。JSON形式、Retry-After、残り回数ヘッダーを固定してください。
3つ目は、成功リクエストだけを数えることです。ログイン失敗、バリデーション失敗、存在しないメールアドレスへのパスワードリセットもコストや攻撃面を持ちます。むしろ失敗のほうを強く制限する場面が多いです。
4つ目は、カウンターキーに個人情報をそのまま入れることです。メールアドレスや電話番号をRedisキーやログに平文で残すと、別の事故になります。必要ならハッシュ化し、保存期間を短くします。
5つ目は、テストで時間を本当に待つことです。60秒窓のテストで毎回60秒待つとCIが遅くなります。時間を注入できる関数にして、テストではnowを進めます。
最後に、SEO botや監視ツールを雑に止めることです。Cloudflareの公式ドキュメントも、verified botsへの影響に注意を促しています。公開記事サイトでは、検索エンジン、ヘルスチェック、社内監視、決済Webhookを別扱いにします。
Claude Codeレビュー用チェックリスト
実装後は、Claude Codeに次の観点で再レビューさせます。
- 429レスポンスが全エンドポイントで同じ形か
Retry-Afterと残り回数ヘッダーが設定されているか- IP、ユーザーID、APIキー、組織IDのキー設計が妥当か
- Redis障害時の挙動が明示されているか
- 認証失敗、入力エラー、外部API失敗も必要に応じて数えているか
- 負荷テストやE2Eテストで制限到達を確認しているか
- 管理者、監視、Webhook、検索botの例外が広すぎないか
このチェックは、単なるコード品質ではなく収益の品質です。AI生成APIやSMSを含むプロダクトでは、レート制限の抜けがそのまま請求額に出ます。
相談CTA: 自社APIの上限設計まで落とし込む
ClaudeCodeLabでは、Claude Codeを使ったAPI実装、セキュリティレビュー、レート制限・課金・監視の設計を研修・導入相談で扱っています。既存のNext.js、Express、Cloudflare Workers、AWS API Gateway構成を前提に、「どの操作を、誰単位で、何回まで許すか」を実コードとテストに落とし込めます。
個人開発ならまずこの記事のNode.js版を動かし、次にRedis版へ進めば十分です。チームで使う場合は、Claude Codeへのプロンプト、レビュー観点、環境変数、運用Runbookまで揃えると、後から制限値を変えても事故りにくくなります。
この記事で紹介した内容を実際に試した結果、メモリ版では6回目の連続アクセスで429が返り、Redis版では12回の連続アクセス中、設定した10回を超えたところでRetry-After付きの429になりました。さらにクライアント例で待機を入れると、無駄な再送が止まりました。小さな検証ですが、レート制限は「入れるかどうか」よりも、レスポンス形式、再試行、ログ、例外設計まで一緒に確認して初めて実務で使えると分かりました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code Permission Receipt Pattern: 許可、証拠、ロールバックを残す運用
Claude Codeの権限運用を安全にする permission receipt。許可範囲、承認待ち、検証コマンド、CTA導線を記録します。
Claude CodeとCodex、結局どっち?事故らない“併用”の現実解
OpenAIのCodexとClaude Code、どっちが得意でどっちに任せる?両方を安全に併用する作業分担と権限・検証のワークフローを、僕の失敗談つきで解説します。
Claude Codeサブエージェント実装ガイド: 記事・コード作業を安全に並列委譲する方法
Claude Codeサブエージェントで記事・コード作業を安全に並列化する実装ガイド。委譲基準、プロンプト、失敗例を解説。