レート制限(Rate Limiting)
レート制限は、クライアントが特定の時間ウィンドウ内にAPIやWebサービスへ送信できる リクエスト数を制限する、基本的なトラフィック制御技術であり、 過負荷、サービス拒否攻撃(DoS/DDoS)、自動化された 不正利用、悪意あるスクレイピング、限られた計算リソースの不適切な使用から インフラを保護します。現代のAPIがモバイルアプリケーション、 パートナー連携、正当なボット、そして潜在的に悪意ある攻撃者から 毎秒数百万件のリクエストを処理するシナリオにおいて、 適切なレート制限の欠如は、運用コストの爆発的増加、正当なユーザーに対する 深刻なパフォーマンス低下、credential stuffingおよびブルート フォース攻撃への脆弱性、さらにはサービスの完全な利用不能を招く可能性があります。 効果的なレート制限の実装には、さまざまなアルゴリズム(token bucket、leaky bucket、fixed window、 sliding window)の深い理解、複数のサーバーが レートカウンターを共有する必要がある分散アーキテクチャに関する考慮、クライアント識別戦略(IP、API key、JWT、 session)、ユーザーティア(free、premium、enterprise)ごとの差別化されたポリシー、そして 制限、現在の消費量、リセット時刻をクライアントに通知する標準化されたHTTP headersを通じた 明確なコミュニケーションの仕組みが必要です。本記事では、堅牢なレート制限システムを構築するための アルゴリズム、実装パターン、ツール、ベストプラクティス(best practices)を探ります。
レート制限アルゴリズム
1. Token Bucket(最も一般的)
# 概念:最大トークン容量を持つ bucket
# Token は一定のレートで追加される
# 各 request は 1 token を消費する
# bucket が空の場合、request は拒否される
容量:100 tokens
補充レート:10 tokens/秒
利点:
- 制御された bursts を許容する(bucket が満杯)
- 実装が簡単
- さまざまなトラフィックパターンに柔軟
欠点:
- システムを過負荷にする bursts を許容する可能性がある
- 最後の補充の tracking が必要
実装:
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate;
this.lastRefill = Date.now();
}
consume(count = 1) {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return true;
}
return false;
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
2. Leaky Bucket
# 概念:一定のレートで漏れる queue
# Requests は bucket(queue)に入る
# 一定のレートで処理される(leak)
# queue が満杯の場合、requests は拒否される
利点:
- bursts を平滑化する(traffic shaping)
- Output rate が一定で予測可能
- backend を spikes から保護する
欠点:
- レイテンシを追加する可能性がある(queueing)
- 実装の複雑さ
用途:
- Traffic shaping
- Network gateways
- 一定の output rate が重要な場合
3. Fixed Window Counter
# 概念:固定時間ウィンドウごとのカウンター
# 例:1分あたり 100 requests
# 各分の開始時にリセット(XX:00, XX:01, XX:02...)
利点:
- 極めてシンプル
- Memory efficient
- 理解しやすい
欠点:
- Edge case:1秒間に 200 requests
(1分目の終わりに 100、2分目の始めに 100)
- ウィンドウの境界で bursts を許容する
Redis 実装:
INCR user:123:2024-01-15:14:30
EXPIRE user:123:2024-01-15:14:30 60
GET user:123:2024-01-15:14:30 # > 100 の場合、reject
4. Sliding Window Log
# 概念:requests のタイムスタンプの log
# ウィンドウ外の requests を削除する
# スライディングウィンドウ内の requests をカウントする
利点:
- 完璧な精度
- fixed window の edge cases がない
- レートを均一に分散する
欠点:
- Memory intensive(すべての timestamps を保存する)
- high traffic でパフォーマンスが低下する
Redis 実装(Sorted Set):
ZADD user:123 <timestamp> <request-id>
ZREMRANGEBYSCORE user:123 0 <timestamp-60s> # 古いものを削除
ZCARD user:123 # Count requests
> 100 の場合、reject
5. Sliding Window Counter(ハイブリッド)
# 概念:fixed window と sliding を組み合わせる
# 以前のウィンドウのカウンターを重み付けして使用する
# スライディングウィンドウでのレートを推定する
例:制限 100/分
現在のウィンドウ(14:30):70 requests
前のウィンドウ(14:29):90 requests
現在のウィンドウでの経過:40s(66.7%)
推定:90 * (1 - 0.667) + 70 = 30 + 70 = 100
利点:
- sliding log に近い精度
- Memory efficient(カウンターは 2 つのみ)
- bursts を平滑化する
欠点:
- 推定(正確ではない)
- fixed window より複雑
分散レート制限(Distributed Rate Limiting)
# 問題:複数のサーバーが state を共有する必要がある
# 解決策:
1. 集中型 Redis(最も一般的)
const redis = require('redis');
const client = redis.createClient();
async function checkRateLimit(userId) {
const key = \`rate:\$:\${getCurrentWindow()}\`;
const count = await client.incr(key);
if (count === 1) {
await client.expire(key, 60); // 60 seconds
}
return count <= 100; // Limit: 100/min
}
2. Redis Lua Script(Atomic)
const luaScript = \`
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = redis.call('incr', key)
if current == 1 then
redis.call('expire', key, ARGV[2])
end
if current > limit then
return 0
end
return 1
\`;
3. Sticky Sessions + Local Counters
- ユーザーを常に同じ server にルーティングする
- サーバー上のローカル counter
- 問題:auto-scaling とうまく機能しない
4. Gossip Protocol
- サーバーが gossip 経由で state を共有する
- Eventual consistency
- より複雑、high scale で使用される
標準 HTTP Headers
# Standards (RFCs)
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000 # Unix timestamp
# 制限を超えた場合
HTTP/1.1 429 Too Many Requests
Retry-After: 60 # retry までの秒数
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000060
{
"error": "Rate limit exceeded",
"retryAfter": 60,
"limit": 100
}
# GitHub スタイル(より情報量が多い)
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873
X-RateLimit-Used: 1
X-RateLimit-Resource: core
Express.js での実装
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient();
// Basic rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
message: 'Too many requests, please try again later.'
});
app.use('/api/', limiter);
// Redis-based distributed rate limiting
const distributedLimiter = rateLimit({
store: new RedisStore({
client: client,
prefix: 'rate-limit:',
}),
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
});
// Different limits per route
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Stricter for auth endpoints
skipSuccessfulRequests: true, // Don't count successful logins
});
app.post('/api/login', authLimiter, loginHandler);
// Custom key function (rate limit by user ID instead of IP)
const userLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => req.user.id, // Requires auth middleware
});
階層型レート制限(Tiered Rate Limiting)
// Different limits based on user tier
function getRateLimit(user) {
const tiers = {
free: { windowMs: 3600000, max: 100 }, // 100/hour
basic: { windowMs: 3600000, max: 1000 }, // 1000/hour
premium: { windowMs: 3600000, max: 10000 }, // 10k/hour
enterprise: { windowMs: 3600000, max: 100000 } // 100k/hour
};
return tiers[user.tier] || tiers.free;
}
app.use(async (req, res, next) => {
const user = await getUserFromToken(req);
const limits = getRateLimit(user);
const limiter = rateLimit({
...limits,
keyGenerator: () => user.id,
});
limiter(req, res, next);
});
ツールとサービス
- Redis:分散カウンター、自動 expire
- Kong:レート制限 plugin を備えた API Gateway
- Nginx rate limiting:limit_req_zone, limit_conn_zone
- Cloudflare:CDN レベルのレート制限
- AWS API Gateway:組み込みの throttling
- express-rate-limit:Express ミドルウェア
- Tyk:オープンソースの API gateway
ベストプラクティス(Best Practices)
- 適切なアルゴリズムを選ぶ:一般的な API には token bucket、精度には sliding window
- 差別化された制限:Auth endpoints はより厳格に、read-only はより寛容に
- 明確なコミュニケーション:情報量の多い headers、役立つエラーメッセージ
- Whitelist:信頼できるパートナーの IP、health checks
- Monitoring:ユーザーが頻繁に制限に達したときにアラートを出す
- Graceful degradation:可能であれば cached data を返す
- Distributed state:マルチサーバーの deployments には Redis を使用する
- Cost-based limiting:コストの高い操作はより多くの tokens を消費する
推奨事項
現代の API には、分散レート制限のために Redis を用いた token bucket を実装してください。 メモリのオーバーヘッドなしに精度が必要な場合は sliding window counter を使用してください。 差別化された制限を設定します:login には 5 req/min、読み取りには 100 req/min、 書き込み操作には 10 req/min。常に情報量の多い headers を返し、クライアント側で exponential backoff を用いた retry logic を実装してください。
