レートリミット完全ガイド:API実装で使うアルゴリズム比較とHTTP応答(429/Retry-After)、分散環境のRedis対策

はじめに — レートリミットとは何か

レートリミット(rate limiting)は、一定期間におけるリクエスト(要求)の数を制御する仕組みです。主にAPI、ウェブサービス、ネットワーク機器、認証システムなどで使われ、過剰な負荷や乱用を防ぎ、サービスの可用性・公平性・セキュリティを保つ役割を果たします。クライアントごと、IPごと、APIキーごと、エンドポイントごとなど様々な単位で適用できます。

なぜレートリミットが必要か

  • サービス保護:短時間に大量のリクエストが来るとCPUやメモリ、データベース接続が枯渇し、正常なユーザに影響が出る。レートリミットで保護できる。
  • 乱用対策:ボットやスクレイピング、ブルートフォース攻撃など意図的な乱用を抑止する。
  • 公平性:一部の利用者が過剰にリソースを消費しないようにすることで、他の利用者に対するサービス品質(QoS)を維持する。
  • コスト管理:外部サービス(例:サードパーティAPI、クラウドリソース)の利用量を制限してコストを抑える。
  • 運用と予測可能性:負荷が予測しやすくなりスケーリングや障害対応が容易になる。

主なレートリミットのアルゴリズム

レートリミットを実現するアルゴリズムはいくつかあり、目的や要求(バースト許容、厳密な平滑性、分散環境での実装容易さ)に応じて選択します。代表的なものを解説します。

  • 固定ウィンドウ(Fixed Window)

    時間を固定長ウィンドウ(例:1分)に区切り、そのウィンドウ内のリクエスト数をカウントします。実装は単純:カウントとウィンドウの開始時刻を保持すればよい。利点は実装が容易なこと。欠点はウィンドウ境界でバーストが生じやすく、例えばウィンドウ末と次ウィンドウ開始直後に大量リクエストが重なると許容量を超えてしまう可能性があることです。

  • スライディングログ(Sliding Window Log)

    各リクエストのタイムスタンプを保存し、現在時刻から過去のウィンドウ長に含まれるリクエスト数を数える手法。精度は高いが、ストレージ(メモリ)と計算コストがリクエスト数に比例して増えるため高トラフィック時に負荷が高くなる。

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

    固定ウィンドウの精度問題を軽減するため、ウィンドウを複数の小ブロック(例:1分を6つの10秒ブロック)に分け、現在ウィンドウは部分的に前後のブロックを加重してカウントする方式。スライディングログより効率的で精度も改善される。

  • トークンバケット(Token Bucket)

    バケットにトークンを一定レートで追加し、リクエスト1件ごとにトークンを消費します。トークンがあれば即時処理(バーストを許可)、トークンがなければ制限(拒否または遅延)。バースト許容と平均レート制御を両立できるため、APIでよく使われます。

  • リークバケット(Leaky Bucket)

    到着するリクエストをキューに入れ、一定レートで処理(漏出)することで平滑化を図る。遅延を伴うことで短期的な突発を吸収するが、キューが溢れると拒否する。概念的にはトークンバケットと似るが、実装上は処理レートを中心に考える点で異なる。

HTTPにおける実装と応答

HTTP APIでは、制限超過時にHTTPステータスコード429(Too Many Requests)を返すのが一般的です(429はRFC 6585で定義されています)。クライアントにいつ再試行すべきかを示すため、レスポンスヘッダにRetry-After(RFC 7231で定義)や独自/慣例のヘッダ(例:X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)を付けることが多いです。これにより、クライアントは適切にバックオフや再試行を実装できます。

  • Retry-After: 再試行可能になるまでの秒数、または有効な日時を示す。
  • X-RateLimit-Limit: 総許容量(例:1000/日)
  • X-RateLimit-Remaining: 残り許容量
  • X-RateLimit-Reset: 許容量がリセットされるUNIXタイムスタンプ

分散環境での課題と対策

マイクロサービスや水平スケール環境では、複数ノードで一貫したレート管理を行う必要があります。単一ノードのメモリカウンタだけでは不十分です。典型的な対策は次のとおりです。

  • 共有ストアの利用:Redisなどを使い、INCR/EXPIREやSorted Setを利用してカウントを管理する。固定ウィンドウはINCR+EXPIREで実装可能、スライディングログはSorted Setでタイムスタンプを格納して古い要素を削除しながらカウントする。
  • 原子操作とLuaスクリプト:RedisのINCRやLuaスクリプトで複数操作を原子的に実行し、レースコンディションを防ぐ。
  • グローバルレートリミッタ:中央のレートリミッタサービスを用意して各ノードから問い合わせる方式。単純だがレイテンシと単一障害点に注意。
  • トークンの分配法:各ノードにトークンを一定量割り当てる方式で分散トークンバケットを模倣する。管理が複雑だがスケーラブル。

実装例(概要)

実装は要件次第ですが、よく使われる簡便な方法を説明します。

  • 固定ウィンドウ(Redis利用の簡易版)

    リクエストごとに Redis のキー(例:rate:{ユーザID}:{YYYYMMDDHHMM})を INCR し、初回は EXPIRE をセット。カウント > 上限なら 429 を返す。利点は実装が容易。欠点は前述のウィンドウ境界問題。

  • トークンバケット(Redis+Lua)

    Redisに残トークン数と最終更新時刻を保存し、Luaでトークンの補充と消費を原子的に行う。補充レートとバケット容量をパラメータ化しておくと柔軟。

クライアント側の対処(良い利用者の作法)

API利用者はサーバの指示に従いネットワーク効率よく振る舞うべきです。ポイントは以下。

  • ステータス429の処理:429を受け取ったらRetry-Afterヘッダがあれば従う。なければエクスポネンシャルバックオフ(指数的に待ち時間を増やす)を実装する。
  • 適切な再試行戦略:冪等な操作のみ自動再試行する。非冪等操作での再試行は重複実行のリスクを考慮。
  • BackoffとJitter:同時に多数のクライアントが再試行すると再びスパイクを生むため、Jitter(ランダム揺らぎ)を入れる。

ベストプラクティス

  • APIごと・リソースごとに適切なレートを設定し、ドキュメント化して公開する。
  • クライアントにわかりやすいレスポンスを返す(429 + Retry-After + 残数ヘッダ)。
  • バーストを許容するかどうかを設計で決め、トークンバケットなど適切なアルゴリズムを選ぶ。
  • 分散環境ではRedisなどの共有ストアや専用のレートリミットプロキシ(Envoyのrate limitサービスなど)を利用する。
  • ログとメトリクスを充実させ、どのクライアントがブロックされているか、どのエンドポイントで限界が発生しているかを監視する。
  • IPアドレスだけで識別する場合はNATや共有プロキシの影響を考慮。認証トークンやAPIキー単位での制限も併用する。

よくある課題と回避策

  • 共有IP(NAT)の問題:家庭や企業の共有IPからの大量アクセスがあると正当な利用者まで制限される。対策は認証トークン単位の制限や柔軟なポリシー。
  • 時計ずれ:分散システムではサーバ間の時計ずれが影響する場合がある。可能な限りUTCとNTPで時刻同期する。
  • 攻撃の回避策:大量の異なるIPから来る分散型攻撃(DDoS)には、上位レイヤ(WAF、CDN、ネットワークレベル)での制御やブラックホールリングが有効。
  • 公平性:重い処理を行うエンドポイントと軽いエンドポイントを同じ上限にすると不公平になる。エンドポイント別に制限を設ける。

人気プロダクトの実装事例(参考)

  • GitHub API:X-RateLimit-* ヘッダで残数やリセット時刻を提供し、上限を超えると429や403(場合により)を返す。
  • Twitter API:詳細なレート制限ポリシーを公開し、エンドポイントごとに異なる上限を設けている。
  • Nginx:limit_req、limit_conn モジュールでリクエスト数や同時接続数を制御。
  • Cloudflare / CDN:エッジでのレート制御によりオリジンサーバの負荷を軽減。

まとめ

レートリミットはサービスの可用性・公平性・セキュリティを守るための基本的な仕組みです。要求される性質(バースト許容、精度、分散環境対応)に応じてアルゴリズムを選び、クライアントにわかりやすく情報を返すことが重要です。実装時は分散時の原子性やコスト、運用上の監視・ロギングを考慮し、適切なテスト(負荷試験)を行って挙動を確認してください。

参考文献