セキュリティ●●●○○

レート制限

レートせいげん

一定時間あたりのリクエスト数を制限する仕組み。乱用と過負荷からAPIを守る。

概要

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

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

なぜ生まれたか

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

そこで「相手の意図は問わず、頻度という観測可能な事実に上限を課す」という割り切りが生まれました。意図の推定は難しくても頻度の計測は簡単で、しかもブルートフォース・スクレイピング・過負荷という異なる問題をまとめて緩和できます。HTTP に 429 Too Many Requests というステータスコードが正式に追加された(RFC 6585、2012年)のは、この仕組みが業界の共通語彙になったことの表れです。

詳細

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

「1分間に60回まで」を素直に実装すると、意外な落とし穴があります。最も単純な固定ウィンドウ方式は、時刻を1分ごとの枠に区切って枠内の回数を数えますが、枠の境界をまたぐと「前の枠の終わり際に60回 + 次の枠の頭に60回」で、実質1分間に120回を通してしまいます。これを改善したスライディングウィンドウ方式は「今この瞬間から過去60秒間」を常に見るため境界問題がなく、直前の枠のカウントを按分して近似する実装(sliding window counter)が実務ではよく使われます。

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

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

実際にトークンバケットへリクエストを撃ち込んで、「容量=バーストの上限、補充レート=平均レート」の関係を体感してみてください。

⚡ 体験: トークンバケットにリクエストを撃ち込む
トークン 5.0 / 5
200 OK: 0429: 0成功率

まだリクエストがありません

まず満杯になるまで待ってから「×5連打」— バーストが全部通ります。続けてもう一度押すと、今度は429の壁に当たります。

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

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

アルゴリズムと同じくらい重要なのが、カウントの単位となるキーの選択です。IPアドレス単位は未認証のエンドポイントにも適用できる基本形ですが、企業やモバイル回線ではNATにより多数の利用者が同じIPを共有するため、巻き添えの誤制限が起きやすい弱点があります。ログイン後ならユーザーID単位、公開APIならAPIキー単位が正確で、料金プランごとに上限を変える段階制にも自然につながります。実務ではこれらを重ねがけします。たとえばログインエンドポイントには「IP単位の緩い制限」と「アカウント単位の厳しい制限」を併用し、ブルートフォースとパスワードスプレー(多数のアカウントに薄く試行を分散させる攻撃)の両方に備えます。認証まわりは特に、MFAコードの試行回数制限のように、レート制限が防御の最後の砦になる場面が多い領域です。

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

上限を超えた相手には HTTP ステータス 429 Too Many Requests を返し、Retry-After ヘッダで「何秒後に再試行してよいか」を伝えるのが標準的な作法です。多くのAPIはさらに RateLimit-Limit(上限)・RateLimit-Remaining(残り回数)・RateLimit-Reset(リセット時刻)といったヘッダを毎レスポンスに付与し、クライアントが上限に達する前に自主的にペースを落とせるようにしています。クライアント側の作法も対になっており、429を受けたら指数バックオフ(待ち時間を倍々に伸ばす再試行)で引き下がるのが行儀のよい実装です。再試行の設計では、同じリクエストが重複実行されても安全なように冪等性の担保が併せて重要になります。

分散環境での状態共有

サーバが1台ならカウンタはメモリに置けば済みますが、ロードバランサの背後に複数台が並ぶ構成では、リクエストが毎回別のサーバに届くため、各サーバが個別に数えると「上限 × 台数」まで通してしまいます。そこでカウンタを Redis のような共有ストアに集約するのが定石です。「読んで、判定して、加算する」の間に他のリクエストが割り込むと数え漏れが生じるため、アトミックなインクリメント操作や Lua スクリプトで判定と更新を1回で行います。ただし全リクエストが共有ストアを往復するとそれ自体がボトルネックになるため、各サーバのローカルカウンタで概算しつつ定期的に同期する近似方式など、厳密さと性能のトレードオフを選ぶことになります。なお、レート制限の実施場所はアプリケーションの手前、リバースプロキシAPIゲートウェイ層に置くのが一般的で、アプリに負荷が届く前に弾けることが過負荷保護の観点では重要です。

最後に位置づけを一つ。レート制限は単体の万能薬ではなく、多層防御の一層です。大規模なDDoS攻撃のような桁違いのトラフィックはレート制限だけでは受け止めきれず、CDNWAFを含む上流での対策と組み合わせて初めて機能します。逆に言えば、どんな防御構成でも「頻度の上限」という最後の調整弁は必ずどこかに必要であり、APIを公開するなら最初に備えるべき防御装置です。