レート制限とは
レート制限(Rate Limiting)は、一定時間内のAPIリクエスト数を制限する仕組みです。サービスの安定性を保ち、悪意のある利用やバグによる過剰アクセスからシステムを保護します。
なぜ必要か: 無制限にリクエストを受け付けると、1人のユーザーがシステム全体のリソースを使い果たしたり、DDoS攻撃によってサービスが停止する可能性があります。
レート制限の目的
| 目的 | 説明 |
|---|---|
| サービス保護 | 過負荷によるダウンを防止 |
| 公平性確保 | すべてのユーザーにリソースを公平に分配 |
| 悪用防止 | スクレイピング、ブルートフォース攻撃の抑止 |
| コスト管理 | インフラコストの予測可能性を確保 |
主要なアルゴリズム
1. 固定ウィンドウ(Fixed Window)
一定時間のウィンドウごとにカウントをリセットします。
設定: ウィンドウ1分(00:00〜00:59)、制限100リクエスト/分
| 時間 | リクエスト | 結果 |
|---|---|---|
| 00:00-00:30 | 90 | ✓ |
| 00:30-00:59 | 10 | ✓(合計100) |
| 01:00 | カウンタリセット | |
| 01:00-01:30 | 100 | ✓ |
問題点: ウィンドウ境界で瞬間的に2倍のリクエストが可能
| 時間 | リクエスト | 問題 |
|---|---|---|
| 00:59 | 100 | ✓ |
| 01:00 | 100 | ✓ |
| → 2秒間で200リクエスト! |
2. スライディングウィンドウログ(Sliding Window Log)
各リクエストのタイムスタンプを記録し、過去N秒間のリクエスト数をカウントします。
現在時刻: 01:00:30、ウィンドウ: 過去60秒(00:00:30〜01:00:30)
| タイムスタンプ | 状態 |
|---|---|
| 00:00:25 | ウィンドウ外(削除) |
| 00:00:35 | ✓ 有効 |
| 00:00:50 | ✓ 有効 |
| 01:00:10 | ✓ 有効 |
メリット: 正確なレート制限 デメリット: メモリ使用量が多い
3. スライディングウィンドウカウンタ(Sliding Window Counter)
固定ウィンドウの改良版。前後のウィンドウのカウントを重み付けして計算します。
| ウィンドウ | リクエスト数 |
|---|---|
| 前(00:00-00:59) | 80 |
| 現在(01:00-01:59) | 30 |
| 現在時刻 | 01:00:20(33%経過) |
計算: 推定リクエスト数 = 80 × 0.67 + 30 = 83.6
4. トークンバケット(Token Bucket)
バケットにトークンが一定レートで追加され、リクエストごとにトークンを消費します。
設定: バケット容量10トークン、補充レート1トークン/秒
| 状態 | トークン | 備考 |
|---|---|---|
| 初期状態 | 10/10 | 満タン |
| 5リクエスト消費後 | 5/10 | 5トークン消費 |
| 3秒後 | 8/10 | 3トークン補充 |
バースト: 現在8リクエスト可能
メリット: バースト対応可能、メモリ効率が良い
5. リーキーバケット(Leaky Bucket)
バケットから一定レートでリクエストが処理されます。
flowchart LR
In["流入<br/>(可変)"] --> Bucket["バケット<br/>(キュー)"] --> Out["流出<br/>(固定レート)"]
メリット: 出力レートが安定 デメリット: バーストに対応しにくい
実装パターン
レスポンスヘッダー
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
制限超過時のレスポンス
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"message": "レート制限を超えました。30秒後に再試行してください。",
"retry_after": 30
}
Redisを使った実装例
async function checkRateLimit(userId, limit, windowSec) {
const key = `ratelimit:${userId}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSec);
}
if (current > limit) {
const ttl = await redis.ttl(key);
return { allowed: false, retryAfter: ttl };
}
return { allowed: true, remaining: limit - current };
}
制限の粒度
ユーザーベース
| ユーザー | 制限 |
|---|---|
| ユーザーA | 100リクエスト/分 |
| ユーザーB | 100リクエスト/分 |
IPアドレスベース
| IPアドレス | 制限 |
|---|---|
| 192.168.1.1 | 100リクエスト/分 |
| 192.168.1.2 | 100リクエスト/分 |
エンドポイントベース
| エンドポイント | 制限 | 備考 |
|---|---|---|
| GET /api/users | 100リクエスト/分 | |
| POST /api/users | 10リクエスト/分 | 作成は厳しく |
階層型
| プラン | 制限 |
|---|---|
| Free tier | 100リクエスト/日 |
| Pro tier | 10,000リクエスト/日 |
| Enterprise | 無制限 |
分散システムでの考慮点
中央集権型
flowchart LR
S1["サーバー1"] --> Redis["Redis<br/>(共有カウンタ)"]
S2["サーバー2"] --> Redis
S3["サーバー3"] --> Redis
メリット: 正確 デメリット: Redisへのレイテンシ
ローカルキャッシュ + 同期
flowchart LR
S1["サーバー1<br/>ローカルカウンタ"] <-->|定期同期| S2["サーバー2<br/>ローカルカウンタ"]
メリット: 低レイテンシ デメリット: 若干の超過を許容
クライアント側の対応
指数バックオフ
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, i);
await sleep(retryAfter * 1000);
continue;
}
return response;
}
throw new Error('Rate limit exceeded after retries');
}
まとめ
レート制限は、APIの安定性と公平性を確保するための重要な仕組みです。トークンバケットやスライディングウィンドウなど、ユースケースに応じたアルゴリズムを選択し、適切な粒度で制限を設定することで、サービスを保護しながら良好なユーザー体験を提供できます。
← 一覧に戻る