セキュリティ●●●○○

レート制限

Rate Limiting ・ レートせいげん

一定時間あたりのリクエスト数に上限を設け、乱用と過負荷からAPIやサーバを守る仕組み。

✦ 30秒で試す — 航海ログイン画面の「しばらく待って」は、あなたを守っているこの約2秒のあいだに、通るのは何回?予想してみる

概要

レート制限(rate limiting)は、「同じ相手からのリクエストは1分間に60回まで」のように、一定時間あたりに受け付けるリクエスト数へ上限を設ける仕組みです。上限を超えたリクエストは拒否するか、待たせてから処理します。公開APIを提供しているサービスのドキュメントに、ほぼ必ず「Rate Limits」の章があるのは、これが現代の API 運用の標準装備だからです。

レート制限には性格の異なる2つの動機があります。1つはセキュリティ、つまり乱用の防止です。ログインを短時間に何千回も試すブルートフォース攻撃や、サイト全体を機械的に吸い上げるスクレイピングは、どれも「異常な頻度」という共通の特徴を持つため、頻度そのものに上限を設けることで広く抑えられます。もう1つは可用性、つまり過負荷からの保護です。悪意がなくても、バグで無限ループするクライアントや突発的なアクセス集中はサーバを圧迫します。レート制限は「一部の相手が資源を食い尽くして全員が困る」事態を防ぎ、利用者の間の公平を保つ調整弁として働きます。

なぜ生まれたか

Web サービスが人間のブラウザ操作だけを相手にしていた時代、リクエストの頻度は自然と人間の速さに収まっていました。しかし API が公開され、プログラムがプログラムを呼ぶ世界になると、この前提が崩れます。1つのスクリプトが人間の何万倍もの速さでリクエストを出せ、しかもそれが攻撃なのか、善意のバグなのか、単なるヘビーユーザーなのかをサーバは区別できません。上限のない API は、たった1つの暴走クライアントで全利用者向けのサービスが止まる弱点を抱えていました。

そこで「相手の意図は問わず、頻度という観測できる事実に上限を課す」という割り切りが生まれました。意図の推定は難しくても頻度を数えるのは簡単で、しかもブルートフォース・スクレイピング・過負荷という別々の問題をまとめて緩和できます。数え方そのものは、もともと通信網の流量制御(ネットワーク機器が回線に流すパケットの速さを整える技術)で使われていたトークンバケットやリーキーバケットが、そのまま API の世界に持ち込まれました。HTTP に 429 Too Many Requests というステータスコードが正式に追加された(RFC 6585、2012年)のは、この仕組みが業界の共通語彙になったことの表れです。

詳細

上限の数え方 — 代表的な4つのアルゴリズム

「10秒に5回まで」を素直に実装すると、意外な落とし穴があります。最も単純な固定ウィンドウ方式は、時刻を10秒ごとの窓に区切って窓の中の回数を数えます。ところが窓の境界をまたぐと、「前の窓の終わり際に5回+次の窓の始まりに5回」で、2秒足らずのうちに10回、つまり上限の2倍を通してしまいます。窓が切り替わった瞬間にカウンタが 0 に戻るからです。

窓1(0〜10秒)上限5回窓2(10〜20秒)上限5回ここでカウンタが 0 に戻る5回 ✓5回 ✓窓の中ではどちらも上限内約2秒で10回=上限の2倍上限「10秒に5回」を固定ウィンドウで数えた場合
固定ウィンドウの境界問題 — 窓が変わるとカウンタが 0 に戻るので、境界の前後で上限の2倍が通る

これを直したスライディングウィンドウ方式は、「いまこの瞬間から過去10秒」を常に見るので境界の問題がありません。全リクエストの時刻を覚えておく実装(sliding window log)は正確ですがメモリを食うため、実務では直前の窓の回数を経過時間で按分して近似する実装(sliding window counter)がよく使われます。

トークンバケット方式は発想を変えて、リクエストの「許可証(トークン)」をバケツに一定の速さで補充し、リクエストのたびに1枚使うモデルです。バケツには容量があり、満杯以上には貯まりません。この設計のうまさは、平常時に貯めたトークンで短時間のバースト(瞬間的なまとまったリクエスト)を容量まで許しつつ、長い目で見た平均の速さは補充の速さで確実に抑えられることです。人間の操作は「画面を開いた瞬間に数回まとめて API を呼ぶ」ようなバースト性を持つため、バーストを一切許さない制限は正当な利用まで弾いてしまいます。トークンバケットはこの現実によく合い、最も広く使われるアルゴリズムです。

トークン補充: 毎秒 r 個(=長期的な平均レートの上限)容量 b(満杯以上は貯まらない)=許容できるバーストの上限リクエスト1個消費処理へ通過トークンあり429 で拒否トークンなし平常時に貯めたトークンで瞬間的なバーストを許容しつつ、平均レートは補充速度 r で抑えられる
トークンバケット — 補充速度が平均レートを、バケツ容量がバーストの上限を決める

同じ連打を2つの数え方に並べて流し、違いを確かめてください。窓の境界(10秒)の直前と直後に連打を置くと、固定ウィンドウだけが上限の2倍を通してしまいます。右端の水位は、固定ウィンドウでは「この窓で数えた回数」、トークンバケットでは「残りのトークン」です。

⚡ 体験: 同じ連打を、2つの数え方に並べて流す
リクエストの時間軸 — タップで「連打×5」を置く(つまんで動かす・タップで消す)0秒5秒10秒15秒20秒固定ウィンドウ 10秒ごとの窓で数え、窓が変わると 0 に戻す窓1(上限5回)窓2(上限5回)通過429トークンバケット 容量5枚・2秒に1枚補充、1回ごとに1枚使う通過4290/5 回この窓5.0 枚トークン

上の時間軸をタップして「連打×5」を置き、流すを押してください。上限はどちらも10秒に5回です。窓の境界(10秒)の前後に置くのがおすすめです。

リーキーバケット方式はトークンバケットの鏡像で、リクエストをいったんバケツ(待ち行列)に溜め、底の穴から一定の速さで漏らすように処理します。出ていく速さが完全に一定になるため、後ろのシステムを一定以上の速さで叩きたくない場合(外部 API を呼ぶ側など)に向きますが、バーストは待ち行列に変わるので遅延が増えます。「拒否するか、平滑化するか」がトークンバケットとの本質的な違いです。

制限を「誰に」かけるか — キーの設計

アルゴリズムと同じくらい大切なのが、数える単位となるキーの選び方です。IPアドレス単位は未ログインのエンドポイントにも使える基本形ですが、企業やモバイル回線では NAT によって多数の利用者が同じ IP アドレスを共有するため、無関係な人まで巻き添えで制限されやすい弱点があります。ログイン後ならユーザー ID 単位、公開 API なら APIキー単位が正確で、料金プランごとに上限を変える段階制にも自然につながります。

実務ではこれらを重ねがけします。たとえばログインのエンドポイントには「IP 単位の緩い制限」と「アカウント単位の厳しい制限」を併用し、ブルートフォースとパスワードスプレー(多数のアカウントに少しずつ試行を分散させる攻撃)の両方に備えます。認証まわりは特に、MFA のコードの試行回数制限のように、レート制限が防御の最後の砦になる場面が多い領域です。6桁のコードは100万通りしかないので、試行回数を絞らなければ総当たりで破れてしまいます。

断り方の作法 — 429 と Retry-After

上限を超えた相手には HTTP ステータス 429 Too Many Requests を返し、Retry-After ヘッダで「何秒後に再試行してよいか」を伝えるのが標準の作法です。多くの API はさらに、毎回のレスポンスに上限・残り回数・リセット時刻を載せて、クライアントが上限に達する前に自分でペースを落とせるようにしています。

ただし、このヘッダの名前はまだ標準が定まっていません。実務で広く使われているのは X-RateLimit-Limit・X-RateLimit-Remaining・X-RateLimit-Reset のような X- 付きの独自ヘッダで、サービスごとに意味が少しずつ違います(Reset が「残り秒数」か「時刻」か、など)。IETF では標準化の草案(RateLimit header fields for HTTP)が議論されていて、初期の版にあった RateLimit-Limit などの3つのヘッダは、現在の版では方針(RateLimit-Policy)と現在の残量(RateLimit)の2つのヘッダにまとめ直されています。クライアントを書くときは、相手のドキュメントでヘッダの意味を確かめるのが確実です。

クライアント側の作法も対になっています。429 を受けたら リトライと指数バックオフ(待ち時間を倍々に伸ばしながら再試行する方法)で引き下がるのが行儀のよい実装で、Retry-After があればその値を優先します。多数のクライアントが同じ時刻に一斉に再試行すると再び上限に殺到するため、待ち時間にランダムな揺らぎ(ジッタ)を足すのが定石です。再試行の設計では、同じリクエストが重複して実行されても安全なように冪等性の担保が併せて重要になります。

分散環境での状態共有

サーバが1台ならカウンタはメモリに置けば済みますが、ロードバランサの背後に複数台が並ぶ構成では、リクエストが毎回別のサーバに届くため、各サーバが別々に数えると「上限 × 台数」まで通してしまいます。そこでカウンタを Redis のような共有ストアに集めるのが定石です。「読んで、判定して、加算する」の間にほかのリクエストが割り込むと数え漏れが出るため、アトミックな加算や Lua スクリプトで判定と更新を1回で行います。ただし全リクエストが共有ストアを往復すると、それ自体が遅延とボトルネックの原因になるため、各サーバのローカルなカウンタで概算しつつ定期的に同期する近似方式など、厳密さと性能のトレードオフを選ぶことになります。

レート制限を置く場所は、アプリケーションの手前のリバースプロキシやAPIゲートウェイが一般的です。アプリに負荷が届く前に弾けることが、過負荷保護の観点では大切だからです。とはいえレート制限は単体の万能薬ではなく、多層防御の一層です。大規模なDDoS攻撃のような桁違いの通信は、レート制限を置いた機器に届く前に回線をあふれさせるので、CDN や WAF を含む上流での対策と組み合わせて初めて機能します。逆に言えば、どんな防御構成でも「頻度の上限」という最後の調整弁はどこかに必要で、API を公開するなら最初に備えるべき防御装置です。

試問

同じリクエストの列を、2つの数え方のレート制限に同時に流します。上限はどちらも「10秒に5回」で、固定ウィンドウは0〜10秒・10〜20秒の窓ごとに数えて窓が変わると0に戻し、トークンバケットは容量5枚・2秒に1枚補充のトークンを1回ごとに1枚使います。上の時間軸をタップすると「連打×5」(0.2秒おきに5回)を置け、印はつまんで動かせます。「流す」で判定が始まります。

✦ 命題挑戦中

「10秒に5回」の固定ウィンドウが、4秒以内に10回(上限の2倍)を通してしまう連打の並びを見つけ、同じ並びをトークンバケットが抑えることを確かめよ

  • 流した結果、固定ウィンドウが 4秒以内に 10回(上限の2倍)を通してしまう(いま最大 0回)
  • 同じ4秒に、トークンバケットは 3回以上を 429 で拒否している
⚡ 体験: 同じ連打を、2つの数え方に並べて流す
リクエストの時間軸 — タップで「連打×5」を置く(つまんで動かす・タップで消す)0秒5秒10秒15秒20秒固定ウィンドウ 10秒ごとの窓で数え、窓が変わると 0 に戻す窓1(上限5回)窓2(上限5回)通過429トークンバケット 容量5枚・2秒に1枚補充、1回ごとに1枚使う通過4290/5 回この窓5.0 枚トークン

上の時間軸をタップして「連打×5」を置き、流すを押してください。上限はどちらも10秒に5回です。