1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rate Limiting とは? — API を守る流量制御の仕組みとアルゴリズム

1
Posted at

はじめに

API を公開すると、意図的な攻撃(DDoS)だけでなく、クライアントのバグによる無限ループや、想定以上のトラフィックによってサーバーが過負荷になるリスクがあります。

Rate Limiting(レート制限) は、一定期間内のリクエスト数を制限することで、サーバーを過負荷から守る仕組みです。

Rate Limiting が必要な理由

脅威 Rate Limiting がない場合
DDoS 攻撃 サーバーが応答不能になる
クライアントのバグ 無限ループで大量リクエストが飛ぶ
スクレイピング コンテンツが大量に抜き取られる
API の不公平な使用 1ユーザーがリソースを独占する
コスト超過 従量課金の外部 API 呼び出しが爆発する

HTTP レスポンスの仕組み

Rate Limiting に関連する HTTP ステータスコードとヘッダーを理解しておきましょう。

ステータスコード 429

HTTP/1.1 429 Too Many Requests
Retry-After: 30

クライアントがレート制限を超えた場合、サーバーは 429 Too Many Requests を返します。

レート制限関連ヘッダー

X-RateLimit-Limit: 100        # 期間内の最大リクエスト数
X-RateLimit-Remaining: 23     # 残りのリクエスト数
X-RateLimit-Reset: 1713400000 # 制限がリセットされるUNIXタイムスタンプ
Retry-After: 30                # 次にリトライ可能になるまでの秒数

クライアント側はこれらのヘッダーを確認し、制限を超えないようにリクエストを調整します。制限を超えてしまった場合は Exponential Backoff でリトライするのが推奨されます。

4つの主要アルゴリズム

1. Fixed Window Counter(固定ウィンドウ)

最もシンプルなアルゴリズムです。時間を固定長のウィンドウに区切り、各ウィンドウ内のリクエスト数をカウントします。

制限: 100リクエスト / 分

|--- 12:00〜12:01 ---|--- 12:01〜12:02 ---|
|  カウント: 78/100   |  カウント: 0/100    |
|  → OK              |  → リセットされる    |
class FixedWindowLimiter {
  private count = 0;
  private windowStart = Date.now();

  constructor(
    private maxRequests: number,
    private windowMs: number
  ) {}

  allow(): boolean {
    const now = Date.now();

    // ウィンドウを超えたらリセット
    if (now - this.windowStart >= this.windowMs) {
      this.count = 0;
      this.windowStart = now;
    }

    if (this.count < this.maxRequests) {
      this.count++;
      return true;
    }
    return false;
  }
}

const limiter = new FixedWindowLimiter(100, 60_000); // 100req/min

問題点: 境界問題(Boundary Problem)

制限: 100リクエスト / 分

|--- 12:00〜12:01 ---|--- 12:01〜12:02 ---|
                  ↑ここで100件 + ↑ここで100件
            12:00:55       12:01:05

→ 10秒間に200件のリクエストが通ってしまう!

ウィンドウの境界付近で集中してリクエストすると、実質的に制限の2倍のリクエストが通ってしまいます。

2. Sliding Window Counter(スライディングウィンドウ)

Fixed Window の境界問題を解決するアルゴリズムです。前のウィンドウのカウントを比率で考慮します。

制限: 100リクエスト / 分
現在時刻: 12:01:15(現在のウィンドウの25%が経過)

前のウィンドウ(12:00〜12:01): 84件
現在のウィンドウ(12:01〜12:02): 36件

推定カウント = 84 × 0.75(残り75%分) + 36 = 99件
→ あと1件まで許可

なぜ比率で計算するのか

「直近1分間に何件リクエストがあったか」を厳密に知るには、全リクエストのタイムスタンプを保持する必要があります(後述の Sliding Window Log がこれにあたります)。リクエストが多くなるほどメモリを食うため、API のように高頻度な用途では現実的ではありません。

そこで、「前のウィンドウ内のリクエストは時間軸上に均等に分布していた」と仮定 することで、カウンタ2つだけで直近1分を近似します。

12:00            12:01            12:02
 |================|================|
 ←  前ウィンドウ  →←  現ウィンドウ  →
        84件             36件

現在時刻: 12:01:15(現ウィンドウの25%経過)

「直近1分」= 12:00:15 〜 12:01:15 を見たい
            ↑                ↑
   前ウィンドウの後半45秒  現ウィンドウの15秒すべて
   = 前ウィンドウの75%

前ウィンドウの 84件が均等分布だと仮定すれば、後半45秒には 84 × 0.75 = 63件 あったはず。現ウィンドウは丸ごと含まれるので 36件。合計 63 + 36 = 99件 を「直近1分の推定値」として使います。

他のアルゴリズムとの比較

手法 メモリ 精度 境界バースト
Fixed Window カウンタ1個 厳密 発生する
Sliding Window Log リクエスト数ぶんのタイムスタンプ 厳密 なし
Sliding Window Counter カウンタ2個 近似(数%ズレ) なし

Log と同じく境界バーストを防ぎつつ、メモリは Fixed Window と同等。「均等分布の仮定」と引き換えに計算コストとメモリを大幅に削減した のがこのアルゴリズムの旨味です。

class SlidingWindowLimiter {
  private previousCount = 0;
  private currentCount = 0;
  private windowStart = Date.now();

  constructor(
    private maxRequests: number,
    private windowMs: number
  ) {}

  allow(): boolean {
    const now = Date.now();
    const elapsed = now - this.windowStart;

    if (elapsed >= this.windowMs) {
      this.previousCount = this.currentCount;
      this.currentCount = 0;
      this.windowStart = now;
    }

    // 前のウィンドウの残り比率を加味
    const ratio = 1 - elapsed / this.windowMs;
    const estimatedCount = this.previousCount * ratio + this.currentCount;

    if (estimatedCount < this.maxRequests) {
      this.currentCount++;
      return true;
    }
    return false;
  }
}

多くの API で推奨されるアルゴリズムです。 精度、シンプルさ、メモリ効率のバランスが良く、境界問題も緩和されます。

3. Token Bucket(トークンバケット)

バケツにトークンが一定レートで補充され、リクエストのたびにトークンを1つ消費します。トークンがなくなるとリクエストは拒否されます。

バケット容量: 10(バースト上限)
補充レート: 1トークン / 秒

[●●●●●●●●●●] ← 初期状態(10トークン)
      ↓ 3リクエスト
[●●●●●●●○○○] ← 7トークン残り
      ↓ 1秒後に1トークン補充
[●●●●●●●●○○] ← 8トークン
      ↓ バーストで8リクエスト
[○○○○○○○○○○] ← 0トークン → 次のリクエストは拒否
class TokenBucketLimiter {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private capacity: number,       // バケット容量(バースト上限)
    private refillRate: number,     // 補充レート(トークン/秒)
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  allow(): boolean {
    this.refill();

    if (this.tokens >= 1) {
      this.tokens -= 1;
      return true;
    }
    return false;
  }

  private refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    const newTokens = elapsed * this.refillRate;

    this.tokens = Math.min(this.capacity, this.tokens + newTokens);
    this.lastRefill = now;
  }
}

// 容量10、毎秒2トークン補充
const limiter = new TokenBucketLimiter(10, 2);

特徴: 一時的なバースト(突発的なリクエスト集中)を許容しつつ、平均レートを制御できます。AWS や多くのクラウドサービスで採用されています。

4. Leaky Bucket(リーキーバケット)

リクエストをキューに入れ、一定レートで処理します。バケツの底に穴が開いているイメージです。

処理レート: 1リクエスト / 秒
キュー容量: 5

リクエスト → [5][4][3][2][1] → 処理(1req/s)
              キュー(FIFO)     ↓
                               一定レートで出力

キューが満杯の状態で新しいリクエスト → 拒否(溢れる)
class LeakyBucketLimiter {
  private queue: Array<() => void> = [];
  private processing = false;

  constructor(
    private capacity: number,      // キュー容量
    private intervalMs: number     // 処理間隔(ミリ秒)
  ) {}

  allow(callback: () => void): boolean {
    if (this.queue.length >= this.capacity) {
      return false; // キューが満杯
    }

    this.queue.push(callback);
    this.processQueue();
    return true;
  }

  private processQueue() {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;
    const task = this.queue.shift()!;
    task();

    setTimeout(() => {
      this.processing = false;
      this.processQueue();
    }, this.intervalMs);
  }
}

特徴: 出力が常に一定レートになるため、バーストが発生しません。ネットワーク機器やトラフィックシェーピングでよく使われます。

アルゴリズムの比較

アルゴリズム バースト 精度 メモリ 実装の複雑さ 推奨場面
Fixed Window 境界で2倍 簡単 内部API、厳密さが不要な場合
Sliding Window 緩和 一般的な API(推奨)
Token Bucket 許容 バーストを許容したい場合
Leaky Bucket なし やや複雑 一定レートを保証したい場合

迷ったら Sliding Window Counter を選ぶのがバランスが良いです。

Rate Limiting の設計ポイント

何を基準に制限するか

基準 用途
IP アドレス 未認証のリクエスト制限 100req/min per IP
API キー ユーザーごとの制限 1000req/hour per API key
ユーザー ID 認証済みユーザーの制限 500req/hour per user
エンドポイント 高コストな操作の制限 POST /orders: 10req/min

制限値の決め方

  1. 通常のトラフィックを計測する: 平均的なリクエスト数を把握
  2. ピーク時の倍率をかける: 通常の2〜5倍程度を上限に
  3. 段階的に調整する: 最初は緩めに設定し、モニタリングしながら絞る

レスポンスの設計

// Rate Limit 超過時のレスポンス例
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "リクエスト数が上限を超えました。しばらく待ってから再試行してください。",
    "retryAfter": 30
  }
}

クライアントが適切にリトライできるよう、Retry-After ヘッダーやレスポンスボディで待機時間を伝えます。

実装の選択肢

アプリケーションレベル

// Express.js + express-rate-limit
import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 60 * 1000,   // 1分
  max: 100,               // 100リクエスト/分
  standardHeaders: true,   // RateLimit-* ヘッダーを返す
  legacyHeaders: false,
  message: { error: "リクエスト数が上限を超えました" },
});

app.use("/api/", limiter);

インフラレベル

ツール 方法
Nginx limit_req_zone ディレクティブ
AWS API Gateway スロットリング設定
Cloudflare Rate Limiting ルール
Redis カウンターを Redis で共有(分散環境)

分散環境での Rate Limiting

複数のサーバーで Rate Limiting を行う場合、カウンターを共有する必要があります。Redis がよく使われます。

// Redis を使った分散 Rate Limiting(概念)
async function checkRateLimit(userId: string): Promise<boolean> {
  const key = `rate_limit:${userId}`;
  const current = await redis.incr(key);

  if (current === 1) {
    await redis.expire(key, 60); // 1分でリセット
  }

  return current <= 100; // 100req/min
}

Rate Limiting と Exponential Backoff の関係

Rate Limiting はサーバー側の防御、Exponential Backoffクライアント側の対応です。

クライアント                          サーバー
   │                                  │
   │──── リクエスト ────────────────→  │
   │                                  │ Rate Limit チェック
   │←── 429 + Retry-After: 30 ─────  │
   │                                  │
   │  Exponential Backoff で待機      │
   │  (Jitter を加えてリトライ)       │
   │                                  │
   │──── リトライ ──────────────────→  │
   │←── 200 OK ─────────────────────  │

両方を適切に実装することで、システム全体の安定性が向上します。

まとめ

観点 ポイント
なぜ必要か サーバーを過負荷・攻撃・不公平な使用から守る
アルゴリズム Sliding Window Counter が一般的に推奨
制限基準 IP、API キー、ユーザー ID、エンドポイント
レスポンス 429 + Retry-After ヘッダーで待機時間を伝える
分散環境 Redis でカウンターを共有
クライアント側 Exponential Backoff + Jitter でリトライ

Rate Limiting は API の信頼性とセキュリティの基盤です。アルゴリズムの選択、制限値の設計、クライアントへの適切なフィードバックを組み合わせて、堅牢な API を構築しましょう。

参考記事・データ

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?