レート制限 どのAPIにも429エラーが表示されるのを止める方法
HTTP 429 リクエストが多すぎ — Retry-After ヘッダーを読み取り、指数関数的なバックオフとジャイターを実装し、トークンバケットとレーキバケットアルゴリズムを理解し、自らのAPIにレート制限を実装する方法。
あなたは429を取得しました。あなたのスクリプトは過去1時間でAPIに繰り返しアクセスしており、ログには赤いエラーが満ちており、デプロイは20分後に開始されます。その瞬間、これは抽象的な話ではなくなります。
HTTP 429 Too Many Requestsは、サーバーが指定した時間枠内で許容できるリクエスト数を超えてリクエストを送信したことを意味します。正しい応答を読み取り、適切にリトライを行うことは、多くの開発者が実際に問題を経験した後に習得するスキルです。以下がその全貌です。
429が実際に伝えていること
ステータスコードはメッセージの半分に過ぎません。本質的な情報はヘッダーにあります:
Retry-After: 30— リトライ前に30秒待つこと。また、HTTP-dateの形でもあります:Retry-After: Mon, 08 Jun 2026 15:00:00 GMTX-RateLimit-Limit: 100— 時間枠内の合計リクエスト数X-RateLimit-Remaining: 0— 現在の時間枠内の残りリクエスト数(現在はゼロです)X-RateLimit-Reset: 1749391200— 時間枠がリセットされるUnixタイムスタンプ
すべてのAPIがこれらすべてを送信するわけではありません。GitHubはすべての項目を送信します。Stripeは Retry-Afterを送信します。一部のREST APIは何も送信せず、あなたが推測することを期待しています。もし Retry-Afterが存在する場合、それを正確に使用してください — これはサーバーが安全な待機時間を示しているためです。そうでなければ、指数関数的なバックオフがフォールバックになります。
リトライを行う間違った方法
単純な実装は以下のようになります:
async function fetchWithoutBackoff(url) {
while (true) {
const res = await fetch(url);
if (res.ok) return res;
if (res.status === 429) continue; // immediately retry
}
}
これは積極的に有害です。もし10のインスタンスが同時に429を取得し、すべて即座にリトライを行う場合、すべてのリトライが同時に発生します — これが「雷の群れ問題」です。結果として、すぐに再度レート制限され、短いループが無限に繰り返され、クライアントがAPIを意図的に悪用しているように見えます。
指数関数的なバックオフとジャイター
正しいパターンは、各リトライが前回よりも長く待つ(指数関数的)こと、そしてランダムなオフセットが複数クライアント間の同期リトライを防ぐ(ジャイター)ことです。
async function fetchWithBackoff(url, options = {}, maxRetries = 5) {
let attempt = 0;
while (attempt <= maxRetries) {
const res = await fetch(url, options);
if (res.ok) return res;
if (res.status !== 429) {
throw new Error(`Request failed: ${res.status}`);
}
if (attempt === maxRetries) {
throw new Error(`Rate limited after ${maxRetries} retries`);
}
// Use Retry-After if provided; otherwise exponential backoff + jitter
const retryAfter = res.headers.get('Retry-After');
let waitMs;
if (retryAfter) {
const seconds = isNaN(retryAfter)
? (new Date(retryAfter) - Date.now()) / 1000 // HTTP date
: Number(retryAfter); // seconds
waitMs = seconds * 1000;
} else {
const baseDelay = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s, 16s
const jitter = Math.random() * 1000; // 0–1000ms random offset
waitMs = baseDelay + jitter;
}
console.log(`Rate limited. Waiting ${Math.round(waitMs / 1000)}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, waitMs));
attempt++;
}
}
ジャイターの部分が多くの実装で欠落しています。ジャイターがないと、複数の並列プロセスからのリトライがまだクラスタ状に到達します。ジャイターがあると、それらが待機期間を広く分散します。
APIが返す Retry-Afterを返す場合、その値を floor として使用してください — それでも429が発生する場合は、さらに指数関数的なバックオフを適用してください。
トークンバケットと漏れバケット
2つのアルゴリズムがレート制限の実装で支配的です。どのアルゴリズムに遭遇しているかを理解することは、APIが負荷下でどのように振る舞うかを知ること、そして自らのAPIを構築する際にどのアルゴリズムを選ぶかを決定する上で非常に重要です。
トークンバケット
バケットは最大Nトークンを保持します。各リクエストは1トークンを消費します。トークンは固定の速度で補充されます(例:1秒あたり10トークン)。バケットが空の場合、リクエストは拒否またはキューに並ばれます。
バーストに適しています。 しばらくリクエストを送っていない場合、トークンが蓄積され、バーストを発生させることができます。GitHubのAPIはこの仕組みを使っています — 1時間あたり5,000リクエストですが、数時間間隔が空いている場合、一度にすべてのリクエストを送ることができます。これは、トラフィックが急激な場合に必要なインタラクティブな用途に適しています。
漏れバケット
リクエストはキューに入り、固定速度で排出されます。リクエストが到着する速度に関係なく、キューが満杯になると、新たなリクエストはドロップされます。
出力が平滑で、バーストは発生しません。 クォータが残っている場合でも、リクエストは設定された速度で順次排出されます。Nginxの limit_req モジュールはこの仕組みを使っています。これは、ダウンストリームシステムを急激なトラフィックから保護するのに適しており、ウェブ훅の配信、外部API呼び出し、そして予測可能なスループットが重要である用途に適しています。
自らのAPIを構築する際、どのアルゴリズムを選ぶべきか: ユーザー対応エンドポイントでバースト耐性が必要な場合 → トークンバケット。ウェブ훅の配信や第三者API呼び出し → 漏れバケット。バックグラウンドジョブでスムーズなスループットが重要な場合 → 漏れバケット。
安全なリクエストレートの計算
どのリクエストを実際に許容できるかをまず理解してください。APIが「1,000リクエスト/時間」と言う場合、それは16.67リクエスト/分または0.278リクエスト/秒です。20%の安全バッファを加えると、約13リクエスト/分になります — これは、2つの時間枠が重なるような極端なタイミング問題を回避するための余裕を確保します。
を使用して、よく使用されるヘッダーをデフォルト値とともに挿入します。認証セクションで認証タイプと認証情報を設定し、カスタムヘッダーを手動で追加します。curl、Postman、またはコードで使用するための完全なヘッダーセットをコピーします。 レート制限計算機 クォータ数を秒単位および分単位のレートに変換し、リクエスト間の適切な待機時間、そしてコンカレントレベルがバーストリスクに与える影響を確認できます。
自らのAPIにレート制限を実装する場合
あなたがAPIの側にいて、適切な429行動を追加したい場合:
- 適切な粒度を選択してください。 IPごとに設定するのは簡単ですが、NATや共有エグレースのサービスでは問題になります。APIキーごとに設定はより良いが、認証が必要です。ユーザーIDごとに設定は、ユーザーIDがある場合に最適です。複数の粒度を混ぜる場合は、どの粒度が勝つかを理解していないと、問題になります。
- 常に返す必要があります。
Retry-After. 429がなければRetry-Afterすべてのクライアントが独自のバックオフヒューリスティックを実装せざるを得なくなります。結果として、さらに「雷の群れ問題」が発生します。 - Redisを使用して分散レート制限を実装します。 メモリ内のカウンターは複数のサーバーインスタンス間で機能しません。Redisは標準的なパターンです。rate-limiter-flexible(Node)やslowapi(Python/FastAPI)などのライブラリは、これに適切に抽象化されています。
INCR+EXPIRE毎回429を発生させた場合、ログを記録してください。 1つのキーからの429の急増は、クライアントのバグまたは意図的な悪用です。どちらもリアルタイムで把握する必要があります。 認証失敗時にレート制限を適用しないでください。 不正な認証情報に対しては401を返し、429を返さないでください。認証失敗時にレート制限を適用することは、認証情報の回転中に自社ユーザーを無意識にロックアウトする原因になります。 今すぐすべきこと - 429を受信している場合: まず、それが存在する場合はそれを使用し、自らの遅延を発明しないでください
- 指数関数的なバックオフとジャイターを実装してください — 上記のコードはコピー&ペーストで使用可能です すべての応答に対して
ヘッダーをログに記録してください — あなたが想定しているより早くクォータを消費している可能性があります
データが頻繁に変化しない場合、レスポンスをキャッシュしてください
- チェック
Retry-Afterレート制限を実装する場合:Redisベースのライブラリを選択し、すべての429に対して - を返し、キーごとの429レートを監視し、認証失敗時にレート制限を適用しないでください。
- 429は敵ではありません — それはAPIが実際に何が起こったかを正確に伝え、(通常は)何秒待つべきかを示しています。ほとんどのレート制限の問題は、そのメッセージを無視してすぐにリトライすることに起因しています。それを行わないでください。
X-RateLimit-Remaining各応答にヘッダーを表示してください — あなたが想定しているより早くクォータが消費されている可能性があります - データが頻繁に変化しない場合、キャッシュ応答を実行してください
レート制限を実装している場合は、Redisをバックエンドとするライブラリを選択し、返してください Retry-After on every 429, monitor the 429 rate per key, and don’t rate-limit on auth failure.
429は敵ではありません — それはAPIが正確に何が間違っているかを伝えているのです。そして(通常は)待機時間も示しています。ほとんどのレート制限の問題は、このメッセージを無視してすぐにリトライすることにあります。そのような行動はしないでください
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
