要約
トークンバケットまたはスライディングウィンドウアルゴリズムを使用してAPIレート制限を実装します。制限を超過した場合は、標準のIETFレート制限ヘッダー(RateLimit-Limit、RateLimit-Remaining、RateLimit-Reset)と429 Too Many Requestsを返します。Modern PetstoreAPIは、ユーザーごとのクォータと明確なエラー応答でレート制限を実装しています。
はじめに
クライアントが1分間に10,000件のリクエストをAPIに送信しました。データベースがクラッシュし、監視アラートが鳴り響き、他の顧客はAPIにアクセスできなくなりました。これは攻撃を受けているのかもしれませんし、単にリトライループに陥ったバグのあるクライアントを扱っているだけかもしれません。
レート制限はこれを防ぎます。クライアントが一定の時間枠内で行えるリクエストの数を制限します。制限を超過した場合、429 Too Many Requestsを返します。クライアントは処理を中断し、APIは健全な状態を保ちます。
古いSwagger Petstoreはレート制限をまったく実装していません。Modern PetstoreAPIは、標準のIETFヘッダー、ユーザーごとのクォータ、および明確なエラー応答でレート制限を実装しています。
このガイドでは、レート制限アルゴリズム、標準ヘッダー、およびModern PetstoreAPIがレート制限を正しく実装する方法について学びます。
なぜAPIにはレート制限が必要なのか
レート制限は、APIを悪用から保護し、公平な利用を保証します。
悪用からの保護
1. サービス妨害 (DoS) 攻撃
攻撃者がリクエストでAPIを大量に埋め尽くし、利用不能にさせます。レート制限はその影響を抑えます。
2. クレデンシャルスタッフィング
攻撃者が何千ものユーザー名とパスワードの組み合わせを試します。レート制限はこれを遅らせます。
3. データスクレイピング
ボットがデータセット全体をスクレイピングします。レート制限はスクレイピングを非現実的なものにします。
4. コスト管理
APIがコストのかかるサービス(AIモデル、サードパーティAPI)を呼び出す場合、レート制限はコストの暴走を防ぎます。
公平な利用
1. 1つのクライアントによるリソースの独占を防ぐ
レート制限がない場合、1秒あたり1000リクエストを行うクライアントが他のクライアントのリソースを奪う可能性があります。
2. 予測可能なパフォーマンス
レート制限は、すべてのクライアントに対して一貫した応答時間を保証します。
3. 段階的なアクセス
無料ティア: 1時間あたり100リクエスト。有料ティア: 1時間あたり10,000リクエスト。レート制限はこれらのティアを強制します。
運用のメリット
1. キャパシティプランニング
APIが処理する最大負荷を把握できます。
2. コストの予測可能性
レート制限はインフラコストを上限に抑えます。
3. 段階的な劣化
負荷がかかっている場合、レート制限は連鎖的な障害を防ぎます。
レート制限アルゴリズム
異なるアルゴリズムには異なるトレードオフがあります。
1. 固定ウィンドウ
固定された時間枠内のリクエスト数をカウントします。
仕組み:
ウィンドウ1 (00:00-00:59): 100リクエストまで許可
ウィンドウ2 (01:00-01:59): 100リクエストまで許可
実装:
def is_allowed(user_id):
current_minute = get_current_minute()
key = f"rate_limit:{user_id}:{current_minute}"
count = redis.incr(key)
redis.expire(key, 60)
return count <= 100
利点:
- 実装が簡単
- メモリ使用量が少ない
欠点:
- バースト問題: クライアントが00:59に100リクエスト、01:00に100リクエスト(2秒で200リクエスト)を送信できる
2. スライディングウィンドウ
ローリングする時間枠内のリクエスト数をカウントします。
仕組み:
01:30の時点で、00:30から01:30までのリクエストをカウントします(過去60分間)。
実装:
def is_allowed(user_id):
now = time.time()
window_start = now - 3600 # 1時間前
key = f"rate_limit:{user_id}"
# 古いリクエストを削除
redis.zremrangebyscore(key, 0, window_start)
# ウィンドウ内のリクエストをカウント
count = redis.zcard(key)
if count < 100:
redis.zadd(key, {now: now})
redis.expire(key, 3600)
return True
return False
利点:
- バースト問題がない
- 正確なレート制限
欠点:
- メモリ使用量が多い(各リクエストのタイムスタンプを保存するため)
- より複雑
3. トークンバケット
トークンは一定のレートでバケットに追加されます。各リクエストは1つのトークンを消費します。
仕組み:
バケット容量: 100トークン
補充レート: 1秒あたり10トークン
リクエスト: 1トークンを消費
実装:
def is_allowed(user_id):
now = time.time()
key = f"rate_limit:{user_id}"
# 現在の状態を取得
data = redis.hgetall(key)
tokens = float(data.get('tokens', 100))
last_refill = float(data.get('last_refill', now))
# トークンを補充
elapsed = now - last_refill
tokens = min(100, tokens + elapsed * 10) # 1秒あたり10トークン
if tokens >= 1:
tokens -= 1
redis.hset(key, 'tokens', tokens)
redis.hset(key, 'last_refill', now)
redis.expire(key, 3600)
return True
return False
利点:
- バーストを許可(バケット容量まで)
- スムーズなレート制限
- 業界標準
欠点:
- 固定ウィンドウよりも複雑
- 状態の保存が必要
4. リーキーバケット
リクエストはキューに追加され、一定のレートで処理されます。
仕組み:
キュー容量: 100リクエスト
処理レート: 1秒あたり10リクエスト
利点:
- 滑らかな出力レート
- ダウンストリームサービスの保護に適している
欠点:
- レイテンシを追加(リクエストがキューで待機する)
- 実装が複雑
どのアルゴリズムを使用すべきか?
ほとんどのAPIに: トークンバケット
業界標準であり、合理的なバーストを許可し、スムーズなレート制限を提供します。
Modern PetstoreAPIは、ユーザーごとのクォータでトークンバケットを使用しています。
標準レート制限ヘッダー
IETF標準ヘッダー(draft-ietf-httpapi-ratelimit-headers)を使用します。
標準ヘッダー
RateLimit-Limit: 時間枠内で許可される最大リクエスト数
RateLimit-Limit: 100
RateLimit-Remaining: 現在のウィンドウに残っているリクエスト数
RateLimit-Remaining: 45
RateLimit-Reset: レート制限がリセットされるまでの秒数
RateLimit-Reset: 3600
応答例
GET /pets
200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 3600
{
"data": [...]
}
レガシーヘッダー(非推奨)
多くのAPIでは非標準のヘッダーが使用されています:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1710331200
これらは使用しないでください。X-プレフィックスは非推奨であり、形式は標準化されていません。
Modern PetstoreAPIがレート制限を実装する方法
Modern PetstoreAPIは、標準ヘッダーでトークンバケットレート制限を実装しています。
ティア別レート制限
無料ティア:
- 1時間あたり100リクエスト
- 1日あたり1,000リクエスト
Proティア:
- 1時間あたり10,000リクエスト
- 1日あたり100,000リクエスト
エンタープライズティア:
- カスタム制限
実装
成功したリクエスト:
GET /v1/pets
200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 3540
{
"data": [...]
}
レート制限を超過:
GET /v1/pets
429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 120
Retry-After: 120
{
"type": "https://petstoreapi.com/errors/rate-limit-exceeded",
"title": "レート制限を超過しました",
"status": 429,
"detail": "1時間あたりのレート制限100リクエストを超過しました",
"instance": "/v1/pets",
"retryAfter": 120,
"limit": 100,
"window": "1h"
}
ユーザーごと vs IPごと
ユーザーごと(認証済みリクエスト):
ユーザーIDまたはAPIキーによってレート制限。より正確で公平です。
user_id = get_authenticated_user()
is_allowed(user_id)
IPごと(未認証リクエスト):
IPアドレスによってレート制限。共有IP、VPNなどにより精度は低いですが、何もないよりは良いです。
ip_address = request.remote_addr
is_allowed(ip_address)
Modern PetstoreAPIは、認証済みリクエストにはユーザーごとのレート制限を使用し、公開エンドポイントにはIPごとのレート制限を使用しています。
レート制限応答形式
レート制限を超過した場合、429ステータスとRFC 9457エラー形式で応答を返します。
応答構造
{
"type": "https://petstoreapi.com/errors/rate-limit-exceeded",
"title": "レート制限を超過しました",
"status": 429,
"detail": "レート制限を超過しました。後でもう一度お試しください。",
"instance": "/v1/pets",
"retryAfter": 120,
"limit": 100,
"remaining": 0,
"reset": 120,
"window": "1h"
}
ヘッダー
429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 120
Retry-After: 120
Retry-After: クライアントに再試行する時期(秒単位)を伝えます。
Apidogによるレート制限のテスト
Apidogは、レート制限の動作をテストするのに役立ちます。
テストシナリオ
1. 通常の使用:
50リクエストを送信 → すべて成功
RateLimit-Remainingが減少することを確認
2. 制限超過:
101リクエストを送信 → 101番目が429を返す
エラー応答形式を確認
Retry-Afterヘッダーを確認
3. リセット動作:
制限を超過 → リセットを待つ → 制限が復元されたことを確認
4. 異なるティア:
無料ティア(1時間あたり100)をテスト
Proティア(1時間あたり10,000)をテスト
制限が正しく適用されていることを確認
Apidogテスト例
// レート制限ヘッダーをテスト
pm.test("Rate limit headers present", () => {
pm.response.to.have.header("RateLimit-Limit");
pm.response.to.have.header("RateLimit-Remaining");
pm.response.to.have.header("RateLimit-Reset");
});
// レート制限超過をテスト
pm.test("Returns 429 when limit exceeded", () => {
// 101リクエストを送信
for (let i = 0; i < 101; i++) {
pm.sendRequest("GET /v1/pets");
}
pm.response.to.have.status(429);
});
レート制限のベストプラクティス
1. 標準ヘッダーを使用する
カスタムのX-ヘッダーではなく、IETF標準ヘッダーを使用します。
2. 403ではなく429を返す
429は「リクエストが多すぎる」という意味です。403は「禁止」という意味です。これらを混同しないでください。
3. Retry-Afterを含める
クライアントに再試行できる時期を伝えます。
4. 制限をドキュメント化する
ドキュメントでレート制限を明確に示します。
5. 異なるティアを提供する
無料ティア: 低い制限。有料ティア: 高い制限。
6. IPではなくユーザーごとにレート制限する
ユーザーごとの制限の方が正確で公平です。
7. バーストを許可する
トークンバケットは、通常の利用を罰することなく、合理的なバーストを許可します。
8. レート制限ヒットを監視する
クライアントがレート制限にヒットする頻度を追跡します。高い頻度は問題を示します。
9. レート制限ステータスエンドポイントを提供する
GET /v1/rate-limit
200 OK
{
"limit": 100,
"remaining": 45,
"reset": 3540
}
10. レート制限をテストする
デプロイ前にApidogを使用してレート制限の動作をテストします。
結論
レート制限は、APIを悪用から保護し、公平な利用を保証します。トークンバケットアルゴリズムを標準のIETFヘッダー(RateLimit-Limit、RateLimit-Remaining、RateLimit-Reset)とともに使用します。制限を超過した場合は、429 Too Many RequestsとRFC 9457エラー形式を返します。
Modern PetstoreAPIは、ユーザーごとのクォータ、標準ヘッダー、明確なエラー応答でレート制限を正しく実装しています。実装の詳細についてはドキュメントを確認してください。
Apidogでレート制限をテストし、負荷がかかった状態やエッジケースでも正しく機能することを確認してください。
よくある質問
どのレート制限を設定すべきですか?
最初は控えめに設定します。無料ティアは1時間あたり100リクエスト、有料ティアは1時間あたり10,000リクエストから始め、利用パターンとインフラ容量に基づいて調整します。
IPごと、それともユーザーごとにレート制限すべきですか?
認証済みリクエストにはユーザー(APIキー)ごとにレート制限を設けます。IPベースのレート制限は、公開エンドポイントにのみ使用してください。
クライアントがレート制限を超過したらどうなりますか?
Retry-Afterヘッダーとともに429 Too Many Requestsを返します。クライアントを永久にブロックするのではなく、ウィンドウがリセットされた後に再試行できるようにします。
Webhookのレート制限はどのように処理しますか?
Webhookはサーバー間であるため、レート制限は高く設定する必要があります。WebhookとAPIコールで別々の制限を検討してください。
内部サービスにもレート制限を設けるべきですか?
はい、ただしはるかに高い制限を設けてください。レート制限は、内部システムでも連鎖的な障害を防ぎます。
レート制限をテストするにはどうすればよいですか?
Apidogを使用して複数のリクエストを送信し、429応答、レート制限ヘッダー、およびリセット動作を検証します。
APIがCDNの背後にある場合はどうなりますか?
CDNのキャッシュは負荷を軽減しますが、キャッシュミスやPOST/PUT/DELETEリクエストにはレート制限が必要です。
複数のサーバーにまたがるレート制限はどのように実装しますか?
共有データストア(Redis、Memcached)を使用して、すべてのサーバーでレート制限を追跡します。ローカルメモリは分散システムでは機能しないため使用しないでください。
