ストーリー
高橋アーキテクトがモニタリング画面を見せた。リクエスト数が急激に跳ね上がっている。
レート制限とは
基本概念
// レート制限: 一定時間内のリクエスト数に上限を設ける
// スロットリング: 制限を超えたリクエストを遅延させる or 拒否する
// 例: 1時間あたり1000リクエストまで
// 1001回目のリクエスト → 429 Too Many Requests
なぜ必要か
| 目的 | 説明 |
|---|---|
| サービスの安定性 | 一部のクライアントがリソースを独占するのを防ぐ |
| コスト管理 | インフラコストの爆発を防ぐ |
| 公平性 | すべてのクライアントに平等なアクセスを保証 |
| セキュリティ | DDoS攻撃やブルートフォース攻撃を軽減 |
レート制限のアルゴリズム
1. 固定ウィンドウ(Fixed Window)
// 固定の時間枠でカウントをリセット
// 例: 毎時0分にカウンターをリセット
// 10:00 - 10:59 → 上限1000リクエスト
// 11:00 - 11:59 → カウンターリセット、再び1000リクエスト
// 問題点: ウィンドウの境界で2倍のリクエストが通る可能性
// 10:59に1000リクエスト + 11:00に1000リクエスト = 2分間で2000リクエスト
interface FixedWindowCounter {
count: number;
windowStart: number;
}
function isAllowed(counter: FixedWindowCounter, limit: number, windowMs: number): boolean {
const now = Date.now();
if (now - counter.windowStart >= windowMs) {
counter.count = 0;
counter.windowStart = now;
}
counter.count++;
return counter.count <= limit;
}
2. スライディングウィンドウ(Sliding Window)
// 現在時刻から遡って一定期間のリクエスト数をカウント
// 固定ウィンドウの境界問題を解決
// 現在10:30の場合、9:30 - 10:30 のリクエスト数をチェック
// より正確だが、実装が複雑
interface SlidingWindowLog {
timestamps: number[];
}
function isAllowed(log: SlidingWindowLog, limit: number, windowMs: number): boolean {
const now = Date.now();
// ウィンドウ外の古いタイムスタンプを除去
log.timestamps = log.timestamps.filter(t => now - t < windowMs);
if (log.timestamps.length >= limit) {
return false;
}
log.timestamps.push(now);
return true;
}
3. トークンバケット(Token Bucket)
// バケットにトークンが一定速度で補充される
// リクエストごとにトークンを1つ消費
// トークンがなくなったら拒否
// バースト対応: バケットに溜まったトークン分だけ一時的に高速処理可能
interface TokenBucket {
tokens: number;
maxTokens: number;
refillRate: number; // トークン/秒
lastRefill: number;
}
function isAllowed(bucket: TokenBucket): boolean {
const now = Date.now();
const elapsed = (now - bucket.lastRefill) / 1000;
// トークンを補充
bucket.tokens = Math.min(
bucket.maxTokens,
bucket.tokens + elapsed * bucket.refillRate
);
bucket.lastRefill = now;
// トークンを消費
if (bucket.tokens >= 1) {
bucket.tokens -= 1;
return true;
}
return false;
}
アルゴリズムの比較
| アルゴリズム | メモリ使用量 | 精度 | バースト対応 | 実装の複雑さ |
|---|---|---|---|---|
| 固定ウィンドウ | 低 | 低 | なし | 低 |
| スライディングウィンドウ | 高 | 高 | なし | 中 |
| トークンバケット | 低 | 高 | あり | 中 |
レスポンスヘッダーの設計
標準的なレート制限ヘッダー
// レスポンスヘッダーでクライアントに情報を伝える
// RateLimit-Limit: 制限値
// RateLimit-Remaining: 残りリクエスト数
// RateLimit-Reset: リセットまでの秒数
// 正常なレスポンス(200 OK)
HTTP/1.1 200 OK
RateLimit-Limit: 1000
RateLimit-Remaining: 742
RateLimit-Reset: 1854
// 制限超過時のレスポンス(429 Too Many Requests)
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 1000
RateLimit-Remaining: 0
RateLimit-Reset: 1854
Retry-After: 1854
Content-Type: application/json
{
"error": {
"code": "RATE_LIMITED",
"message": "リクエスト数が制限を超えました。1854秒後に再試行してください。",
"retryAfter": 1854
}
}
TypeScript でのミドルウェア実装
interface RateLimitConfig {
windowMs: number; // ウィンドウサイズ(ミリ秒)
maxRequests: number; // ウィンドウ内の最大リクエスト数
}
function rateLimitMiddleware(config: RateLimitConfig) {
const counters = new Map<string, { count: number; resetAt: number }>();
return (req: Request, res: Response, next: NextFunction) => {
const clientId = req.ip || req.headers['x-api-key'] as string;
const now = Date.now();
let counter = counters.get(clientId);
if (!counter || now >= counter.resetAt) {
counter = { count: 0, resetAt: now + config.windowMs };
counters.set(clientId, counter);
}
counter.count++;
const remaining = Math.max(0, config.maxRequests - counter.count);
const resetSeconds = Math.ceil((counter.resetAt - now) / 1000);
// ヘッダーを設定
res.set('RateLimit-Limit', String(config.maxRequests));
res.set('RateLimit-Remaining', String(remaining));
res.set('RateLimit-Reset', String(resetSeconds));
if (counter.count > config.maxRequests) {
res.set('Retry-After', String(resetSeconds));
return res.status(429).json({
error: {
code: 'RATE_LIMITED',
message: `リクエスト数が制限を超えました。${resetSeconds}秒後に再試行してください。`,
retryAfter: resetSeconds,
}
});
}
next();
};
}
プラン別のレート制限
// APIの利用プランに応じて制限を変える
const RATE_LIMITS: Record<string, RateLimitConfig> = {
free: { windowMs: 3600_000, maxRequests: 100 }, // 100 req/hour
basic: { windowMs: 3600_000, maxRequests: 1000 }, // 1,000 req/hour
pro: { windowMs: 3600_000, maxRequests: 10000 }, // 10,000 req/hour
enterprise: { windowMs: 3600_000, maxRequests: 100000 }, // 100,000 req/hour
};
まとめ
| ポイント | 内容 |
|---|---|
| レート制限の目的 | 安定性、コスト管理、公平性、セキュリティ |
| アルゴリズム | 固定ウィンドウ、スライディングウィンドウ、トークンバケット |
| レスポンスヘッダー | RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset |
| 429 Too Many Requests | Retry-After ヘッダーで再試行タイミングを伝える |
| プラン別制限 | 利用プランに応じて制限値を変える |
チェックリスト
- レート制限が必要な理由を説明できる
- 3つのアルゴリズムの違いを理解した
- レート制限のレスポンスヘッダー設計を把握した
- 429ステータスコードとRetry-Afterヘッダーの使い方を理解した
次のステップへ
レート制限とスロットリングを学びました。
次は演習です。ここまで学んだRESTful API設計の知識を総動員して、 実際にAPIを設計してみましょう。
推定読了時間: 30分