普段は社内向けのアプリケーションを開発することが多く、外部公開APIを自分で設計・実装する機会はあまりなかった。
そのため、レートリミットについても、なんとなく「制限に達したら429 Too Many Requests を返すもの」くらいの理解でした。
ただ、公開APIの仕様を見ていると、429だけではなく、Retry-After や RateLimit 系のヘッダーとやらが。
見てみると、たしかにこれがあるとクライアント側がかなり実装しやすそうだなと思ったので、メモとして整理。
429だけだと何が分からないのか
例えばAPIを呼び出していて、突然次のようなレスポンスが返ってきたとする。
HTTP/1.1 429 Too Many Requests
これ自体はHTTP的には間違っていない。
ただ、クライアント側から見ると、この情報だけでは次にどうすればよいのかがわからない。
外部のAPIを叩くときにはいつもドキュメントを確認してレートリミットを超えない様に実装して下記みたいなコメントを残すなど恥ずかしい実装を今までしてた...😭
- あと何秒待てばよいのか
- 制限は1分単位なのか、1時間単位なのか
- すぐリトライしてよいのか
- 今日の上限に達したのか
- APIキー単位の制限なのか、ユーザー単位の制限なのか
リトライとかになると書きみたいな実装になっちゃうと思う。
429
|
v
とりあえず5秒待つ
|
v
まだ429
|
v
30秒待つ
|
v
まだ429...
社内アプリだけを作っているとあまり意識しないのですが、公開APIの場合はSDK、CLI、バッチ処理など、いろいろなクライアントから使われます。
そう考えると、ステータスコードだけでなく「次にどう動けばよいか」をレスポンスで伝えることも大事。
Retry-Afterがあると待ち時間が分かる
429を返すときに、まず分かりやすいのがRetry-After です。
HTTP/1.1 429 Too Many Requests
Retry-After: 60
この場合、クライアントは「60秒後に再試行すればよさそう」と判断できます。
秒数だけでなく、HTTP-date形式でも返せます。
HTTP/1.1 429 Too Many Requests
Retry-After: Fri, 03 Jul 2026 12:00:00 GMT
API用途だと、個人的には秒数の方が扱いやすそうに見える。
const retryAfter = Number(response.headers.get("Retry-After") ?? "0");
if (response.status === 429 && retryAfter > 0) {
await sleep(retryAfter * 1000);
}
これだけでも、クライアント側が待ち時間を推測しなくて済む。リトライ後が1日とかの仕様の場合だとタイムスタンプのほうがありがたいのかも。
RateLimit系のヘッダーで残り回数も分かる
Retry-After は「いつ再試行すればよいか」を伝えるには便利です。
ただ、クライアント側としては、ほかにも知りたい情報がある。
- 全体の上限はいくつなのか
- あと何回使えるのか
- いつ回復するのか
- どの制限に引っかかったのか
このあたりを伝えるために、公開APIではレートリミット情報をヘッダーで返していることがある。
よく見かけるのは、次のような形式。
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 300
ざっくり意味は次のような感じ。
| ヘッダー | 意味 |
|---|---|
X-RateLimit-Limit | 利用可能な上限回数 |
X-RateLimit-Remaining | 残り利用可能回数 |
X-RateLimit-Reset | リセットまでの秒数、またはリセット時刻 |
X-RateLimit-* はサービスごとに意味が少し違うこともあるようです。
例えば Reset が「リセットまでの秒数」なのか「UNIX timestamp」なのかは、APIによって異なる場合があります。
なので、実際に利用する場合は、そのAPIのドキュメントを確認する必要があります。
新しい仕様ではRateLimit / RateLimit-Policyもある
最近の仕様では、次のようなヘッダーもあるらしい。
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=42;t=30
ざっくり言うと、RateLimit-Policy は「どんな制限ルールか」、RateLimit は「今どれくらい使えるか」を表すもののよう。
例えば次の例だと、
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=42;t=30
次のように読める。
q=100: 60秒の窓で100リクエストまでr=42: 現在あと42リクエスト使えるt=30: この窓はあと30秒
なので、新しくAPIを設計する場合は、既存の慣習と新しい仕様の両方を見ておくとよさそう。
429レスポンスの例
分割ヘッダー方式なら、例えば次のような形になります。
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded.",
"retryAfter": 60
}
RateLimit / RateLimit-Policy を使うなら、例えば次のような形です。
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=0;t=60
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded.",
"retryAfter": 60
}
大事なのは、クライアントが「次にどうすればよいか」を判断できることだと思う。
SDKで自動リトライしやすくなる
例えばJavaScript SDKなら、次のような実装ができます。
async function requestWithRetry(input: RequestInfo, init?: RequestInit) {
const response = await fetch(input, init);
if (response.status !== 429) {
return response;
}
const retryAfter = Number(response.headers.get("Retry-After") ?? "0");
if (retryAfter <= 0) {
throw new Error("Rate limit exceeded.");
}
await sleep(retryAfter * 1000);
return fetch(input, init);
}
実際のSDKでは、最大リトライ回数、指数バックオフ、キャンセル、冪等性なども考える必要があります。
ただ、サーバーがRetry-After を返してくれていれば、少なくとも「いつ再試行するか」は推測しなくて済みます。
管理画面で利用状況を表示できる
レートリミット情報があると、管理画面にも利用状況を表示できます。
API Usage
98 / 100 requests
利用者からすると、「あと何回使えるのか」が分かるだけでも安心感があります。
429になった場合も、単に「エラーです」と出すより、
利用上限に達しました。約60秒後に再試行できます。
のように説明できた方が親切です。
CLIツールでも扱いやすい
CLIツールでも、残りリクエスト数やリセット時間を表示できます。
Remaining requests: 42
Reset in: 300s
残り回数が少なくなったら、処理速度を落とすこともできます。
Remaining requests: 3
Slowing down to avoid rate limit...
429になってから止まるのではなく、429になる前にペースを調整できるのは便利そうです。
CI/CDやcronで実行されるCLIでも役に立ちそうです。
バッチ処理でも使いやすい
大量のデータを取得するバッチ処理では、429の扱いがけっこう重要になります。
何も情報がない429だと、どれくらい待てばよいか分かりません。
一方で、残り回数やリセット時間が分かれば、次のような制御ができます。
Remaining = 0
|
v
Resetまで待機
|
v
前回の続きから再開
無駄にリトライし続ける必要がないので、実行時間やコストの面でも扱いやすくなる。
エラー本文にも入れると扱いやすい
ヘッダーだけでも機械的な制御はできます。
ただ、JSONにも情報を含めておくと、さらにUXが向上するはず。
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=0;t=60
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded.",
"retryAfter": 60,
"rateLimit": {
"policy": "default",
"limit": 100,
"remaining": 0,
"resetAfter": 60
}
}
ただ、ヘッダーとJSONの値がズレると逆に混乱するので気を付ける
実際のAPIでもほとんど実装されている
レートリミットヘッダーは、公開APIではわりと一般的に実装されているようです。
例えば、次のようなAPIでレートリミット情報が返されています。
- GitHub API
- Stripe API
- Cloudflare API
- Discord API
- Shopify API
- X API
- freee API
ヘッダー名や値の意味はサービスごとに違いますが、「レートリミット情報をレスポンスで返す」という考え方自体はかなり一般的そうです。
実装時に決めておくこと
レートリミットヘッダーを返す場合は、ヘッダー名だけでなく、次のような点も決めておく必要があります。
- 制限単位はAPIキー単位か、ユーザー単位か、IP単位か
- エンドポイントごとに制限が違うのか
Resetは秒数なのか、UNIX timestampなのか- 複数の制限がある場合にどれを返すのか
Retry-AfterとRateLimitの値が矛盾しないようにする- 429以外の通常レスポンスにも残り回数を返すのか
特に複数の制限がある場合は少し難しそうです。
例えば「1分100回」と「1日10,000回」の両方がある場合、どちらの残り回数を返すのかを決めておかないと、クライアント側が判断しづらくなる。
RateLimit-Policy / RateLimit のようにポリシー名を含める形式は、このあたりを表現しやすいのかもしれません。
まとめ
これまでレートリミットについては、なんとなく「制限に達したら429を返す」くらいに考えていました。
ただ、公開APIの仕様を見ていると、429だけでなくRetry-After や RateLimit 系のヘッダーを返しているものが多くありました。
たしかに、これらの情報があると、クライアント側は次のようなことができます。
- いつ再試行すればよいか分かる
- 残り回数を表示できる
- SDKで自動リトライしやすい
- CLIやバッチ処理で無駄なリトライを避けられる
- ユーザーに分かりやすく説明できる
社内向けアプリだけを作っていると、あまり意識する機会がない部分かもしれません。
ただ、公開APIを設計する場合は、ステータスコードだけでなく「クライアントが次にどう動けるか」まで考えてレスポンスを設計すると、利用者にとって扱いやすいAPIになりそうです。