レート制限(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 を実装してください。