Nginx Rate Limit 入門|limit_req・burst・nodelay の仕組みを図解
サイトを長く運営していると、必ず奇妙なトラフィックに遭遇します。ログインページが突然毎秒数百回もパスワード試行されたり、API があるボットに全速力でクロールされたり、あるいは単に誰かのプログラムがバグって無限リトライループに陥り、サーバーのコネクションを食い尽くしたり。こうなるとアプリケーション層で対処しようとしても手遅れで、リクエストがアプリに届く前に CPU が燃え上がっています。Nginx 内蔵の limit_req モジュールはまさにこの状況のためにあり、数行の設定でフロントラインから異常トラフィックをブロックし、本物のユーザーには通常通り使ってもらえます。
ただ limit_req は設定こそ短いものの、背後の概念は直感的とは言えません。burst・nodelay・delay の組み合わせで挙動はかなり変わり、ネット上のテンプレートをコピペすると、厳しすぎて正規ユーザーに 503 を返したり、緩すぎて事実上設定していないのと同じになったりしがちです。この記事ではリーキーバケットアルゴリズムの原理から始め、各パラメータが裏で何をしているかを分解し、図解で 3 つのモードを比較し、最後に実戦的な設定例まで通して見ていきます。
Rate Limit が必要な理由
Rate Limit は日本語では「レート制限」と訳され、特定のソースが単位時間あたりに送れるリクエスト数を制限することが目的です。一見ファイアウォールに似ていますが、切り口が違います。ファイアウォールは「許可・拒否リスト」型の判定で、条件に合えば通し合わなければ止めます。Rate Limit は「クォータ」型の判定で、最初の数リクエストは通し、クォータを超えたものだけブロックします。悪意はないがバグったプログラムを書いてしまったユーザーにとって、Rate Limit は IP を完全にバンするより遥かに優しく、しばらく経てば自然に回復します。
Rate Limit を入れたい代表的なシーン:
- ログイン・登録・パスワードリセットなど機微なページ:ブルートフォース攻撃や大量の偽アカウント登録を防ぐ
- 公開 API エンドポイント:単一ユーザーがサービス全体のキャパを占有するのを防ぐ
- 検索・ファイルダウンロード・レポート生成などコストの高い処理:悪意ある利用者に連発されてシステムが落ちるのを防ぐ
- サイト全体のエントリーポイント:最外層の保護として、突発トラフィックがバックエンドに直撃するのを避ける
ちなみに Rate Limit は万能ではなく、分散型クローラー(各 IP は数リクエストしか送らないが、IP が数万ある)には無力です。そういうケースは WAF や Cloudflare のようなサービスと組み合わせる必要があります。ただ、日常的な異常トラフィックの 90% は Nginx の Rate Limit 一段で大半をブロックできます。
リーキーバケットアルゴリズム
Nginx の limit_req は古典的な リーキーバケット(Leaky Bucket) アルゴリズムを採用しています。キッチンシンクをイメージしてみましょう。蛇口はいつでも誰かが開ける可能性があり(リクエスト到着)、下の排水口は固定速度で水が抜けます(バックエンドの処理速度)。流入が排出より遅ければ水位は常に空で問題なし。突然大量に流れ込んでも、水位が縁を超えなければまだセーフ。縁を超えて溢れた分は床にこぼれるしかありません(リクエストが拒否される)。
Nginx の設定に対応させると、バケットの「排出速度」が rate、「容量」が burst、burst を超えると溢れて Nginx がエラーステータスコードを返します。Nginx 1.3.15 で limit_req_status ディレクティブが導入されて以来、デフォルトは一貫して 503(Service Unavailable)です。limit_req_status 429; で 429(Too Many Requests)に変更できます。意味的には 429 のほうが正確で、サーバーが本当に落ちたときの 503 と混同しないため、現代のサービスはほぼ明示的に 429 へ変えています。検証してデフォルトが 429 だった場合は、設定ファイルのどこかが既に変更されているか、前段に Cloudflare のようなプロキシがあってステータスコードを書き換えている可能性が高く、nginx -T | grep limit_req_status で確認できます。このアルゴリズムの良さは予測可能性の高さです。短時間にどれだけリクエストが押し寄せても、バックエンドが受け取る速度は決して rate を超えないため、後ろが破裂しません。
2 つのディレクティブ:limit_req_zone と limit_req
Nginx の Rate Limit は 2 つのディレクティブから成ります。limit_req_zone で「バケットの形を定義」し、limit_req で「特定の場所にそのバケットを適用」します。この設計のおかげで、同じ zone を複数の location で共有することも、別々の場所に別々の zone を当てることもできます。
# zone を定義(http {} ブロック内に置く)
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /api/ {
# zone を適用(server または location 内)
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://backend;
}
}
短い数行ですが、各フィールドにこだわりがあります。以下、limit_req_zone の 3 つのフィールドと limit_req のパラメータを順番に分解していきます。
key:リクエストソースを何で識別するか
limit_req_zone の第 1 引数は key、つまり「何を単位にクォータを計算するか」です。最も一般的な選択は $binary_remote_addr(クライアント IP)。なぜ $binary_remote_addr で $remote_addr ではないのか?前者はバイナリ形式で 4 バイト(IPv4)または 16 バイト(IPv6)しか占有しませんが、後者は文字列形式で IPv4 でも 7〜15 バイト必要です。差は共有メモリの使用量に直接反映され、追跡対象の IP が増えると無視できないサイズになります。
IP 以外も key にできます。よくある選択肢:
$binary_remote_addr:クライアント IP 単位(最も一般的)$server_name:ドメイン単位、ドメイン全体で 1 つのクォータを共有$http_x_api_key:API キー単位(ヘッダー認証のサービス向け)$cookie_session_id:ログインセッション単位- 空文字
"":ソースを区別せず全体で 1 つのクォータを共有(重い処理の保護に向く)
注意:Nginx の前段にさらにリバースプロキシや CDN(Cloudflare、AWS ALB など)がある場合、$binary_remote_addr はプロキシサーバーの IP になり、すべてのトラフィックが同一ソースとして計算されます。この場合は $http_x_forwarded_for または $realip_remote_addr を使い、real_ip モジュールで信頼するプロキシを正しく設定する必要があります。そうしないとこのフィールドは簡単に偽装されます。
zone:共有メモリ領域
zone=mylimit:10m は「共有メモリ領域」を定義しており、名前は mylimit、サイズは 10 MB です。この領域は Nginx の複数の worker プロセスが共有してアクセスし、各 key の現在のバケット水位や最終更新時刻などを記録します。
10 MB でどのくらいの key を追跡できるか?公式ドキュメントによると 1 MB で約 16,000 個の IPv4 状態を保持できるので、10 MB で約 16 万。中小サイトには十分余裕で、大規模サイトは状況に応じて 32 MB や 64 MB まで上げます。容量が足りなくなると Nginx は 503 を返し、エラーログにメッセージを残すので、そのときに zone を拡大します。
rate:平均速度
rate=10r/s は各 key が毎秒最大 10 リクエストという意味です。Nginx は r/m(毎分)も単位として受け付け、例えば rate=30r/m。ただし注意点として、Nginx は内部で「リクエスト間の最小間隔」に変換して実行します。10r/s は 100 ms に 1 個、30r/m は 2 秒に 1 個。つまり 30r/m は「1 分以内に 30 個までバースト可、その後 60 秒待つ」ではなく「2 秒に 1 個ずつしか通せない」です。「1 分以内のバースト」を作りたい場合は、後で説明する burst を使う必要があります。
burst:短時間のバーストを許容
burst はリーキーバケットの「容量」、つまり何個のリクエストをバケット内に一時的に並べて待たせられるかです。burst を設定しないとバケット容量 0 になり、rate を超えたリクエストはすぐ拒否されます。しかし実務では「精密に均等分布した」トラフィックは滅多になく、ユーザーがページを 1 回クリックしただけで瞬間的に 5〜10 リクエスト(HTML、CSS、JS、画像、API)が発火し、すべてクォータに加算されます。burst=0 だと正規ユーザーが簡単に巻き込まれます。
そのため実務上 burst は合理的なバッファ、例えば rate の 2〜5 倍を設定するのが一般的です。burst が大きいほどスパイクに耐えられますが、攻撃者がバケットを満たすまでに送れるリクエスト数も増えます。
nodelay:burst 内のリクエストを遅延させない
burst だけで nodelay を付けない場合、rate を超えたリクエストは「キューイング」され、rate の速度で順次処理されます。例えば rate=2r/s burst=4 で瞬間的に 5 個流れ込むと、1 個目は即座に処理され、2〜5 個目はそれぞれ 0.5s、1.0s、1.5s、2.0s に処理されます。ユーザーから見ると後ろのリクエストの応答時間が人為的に引き延ばされます。
nodelay を付けると挙動はこう変わります。burst バケット内のリクエストは「即座に全て処理」されますが、バケットの水位はゼロに戻らず、rate の速度でゆっくり補充されていきます。つまりユーザーは瞬間的なバースト時には遅延を感じませんが、続く数秒以内に新しいリクエストを発火するとバケットがまだ補充されておらず拒否されます。このモードはユーザー体験に最も優しく、多くの Web アプリケーションで第一選択です。
3 つのモードを言葉で説明すると抽象的なので、図で比較するとわかりやすいです:
図から見て取れるように、モード ② と ③ で「処理されるリクエスト数」は同じ(5 成功、1 個 503)で、違いは「いつ処理されるか」です。モード ② はトラフィックを平滑化するためバックエンド負荷が最も安定しますが、ユーザーは遅延を感じます。モード ③ はバックエンドが短時間に 5 リクエストの瞬間負荷を受けますが、ユーザー体験は最良です。実務上、バックエンドが短時間のスパイクに耐えられるなら nodelay はほぼ必須のオプションです。
delay:burst の折衷モード
Nginx 1.15.7 以降に追加された delay パラメータを使うと、「最初の N 個は即時処理、それ以降は遅延処理」というハイブリッドモードが作れます。例えば burst=10 delay=5 の場合、最初の 5 個は即時処理(nodelay 相当)、6〜10 個目はキューに並べて遅延処理、11 個目以降は 503 を返します。「ユーザー側にはある程度のバースト余地を残しつつ、burst 全体が同時にバックエンドへ流れ込むのは避けたい」シーンに向きます。
実戦的な設定例
基本:ログインページの保護
ログインページはブルートフォースで最も狙われやすい対象で、クォータは厳しく設定できます。正規ユーザーは短時間に何十回もログインしません。次の設定は各 IP につき毎秒最大 1 回のログイン試行を許可し、瞬間的なバースト 5 回(ユーザーの素早いリトライなど)を許容します:
http {
# login という zone を定義、毎秒 1 リクエスト
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# 503 ではなく 429(Too Many Requests)で応答、意味的により正確
limit_req_status 429;
server {
location = /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://backend;
}
}
}
階層化:一般 API エンドポイント vs 重いエンドポイント
大規模 API サービスではエンドポイントを段階分けすることが多いです。一般的なクエリには緩いクォータを与え、検索やレポートなど重いエンドポイントは厳しめに。複数の zone を定義して別の location に適用すれば階層管理できます:
http {
# 一般 API:毎秒 20 個
limit_req_zone $binary_remote_addr zone=api_general:10m rate=20r/s;
# 重いエンドポイント:毎秒 2 個
limit_req_zone $binary_remote_addr zone=api_heavy:10m rate=2r/s;
server {
# デフォルトは一般制限を適用
location /api/ {
limit_req zone=api_general burst=40 nodelay;
proxy_pass http://backend;
}
# 検索エンドポイントは追加で重い制限を適用(2 つの limit_req が同時に有効)
location /api/search {
limit_req zone=api_general burst=40 nodelay;
limit_req zone=api_heavy burst=5 nodelay;
proxy_pass http://backend;
}
}
}
同じ location 内に複数の limit_req を書け、それぞれが独立したバケットになり、すべてを通過しないと許可されません。これで検索エンドポイントは「全体 API クォータ」と「検索専用クォータ」の二重制約を受けます。
Cloudflare オレンジクラウドのリバースプロキシと組み合わせる
サイトで Cloudflare のオレンジクラウド(プロキシモード)を有効にしていると、すべてのトラフィックは Cloudflare のエッジノードを経由してオリジンに転送されます。このとき Nginx が見る $remote_addr はすべて Cloudflare ノードの IP(173.245.x.x、104.16.x.x など)になります。$binary_remote_addr をそのまま Rate Limit の key にすると大事故になります。世界中のユーザーが同一ソースとして計算され、数秒でブロックされ尽くします。流れはこんな感じです:
Cloudflare は真のユーザー IP を 3 つのヘッダーのいずれかに入れます:
CF-Connecting-IP:Cloudflare 独自のヘッダー、単一 IP のみで全プランで利用可能、最もおすすめX-Forwarded-For:標準ヘッダーだが複数 IP を含むことがあり、クライアント側でも自前で付けられる(real_ip_recursive onでオリジン側に最も近い信頼可能な IP まで遡る必要がある)True-Client-IP:Cloudflare Enterprise プランのみ、CF-Connecting-IPと同等の挙動
正しいやり方は、Nginx 内蔵の ngx_http_realip_module(一般的な distro の nginx には組み込まれている)と組み合わせ、Nginx に「これらの CIDR からのリクエストは、指定したヘッダーから真の IP を読み取って $remote_addr を上書きしろ」と伝えることです。完全な設定はこちら:
http {
# === Cloudflare IPv4 レンジ ===
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
# === Cloudflare IPv6 レンジ ===
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
# CF-Connecting-IP から真の IP を取得
real_ip_header CF-Connecting-IP;
# この後、$binary_remote_addr が真のユーザー IP になる
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
}
重要:信頼する CIDR レンジを必ず明示的に列挙し、横着して 0.0.0.0/0(全ソースを信頼)と書いてはいけません。さもないと、攻撃者がオリジン IP を直接叩いて(Cloudflare をバイパスして)CF-Connecting-IP ヘッダーを偽装すれば、任意の IP を装って Rate Limit を回避できます。オリジン IP の漏洩が心配なら、ファイアウォール層でも 80/443 ポートへの接続を Cloudflare IP のみに制限する二重保険をおすすめします。
Cloudflare IP リストの自動同期
上の IP リストは手で写すと長く、Cloudflare はたまに新しいレンジを追加するので、設定ファイルにハードコーディングすると徐々に古くなります。Cloudflare 公式 API から自動生成する小スクリプトを書いて cron で週 1 回実行するのがおすすめです:
#!/bin/bash
# /etc/nginx/update-cloudflare-ips.sh
set -e
OUT=/etc/nginx/conf.d/cloudflare-realip.conf
TMP=$(mktemp)
{
echo "# Auto-generated from Cloudflare IP list"
echo "# Updated at $(date -Iseconds)"
curl -fsS https://www.cloudflare.com/ips-v4 | sed 's|^|set_real_ip_from |;s|$|;|'
curl -fsS https://www.cloudflare.com/ips-v6 | sed 's|^|set_real_ip_from |;s|$|;|'
echo "real_ip_header CF-Connecting-IP;"
} > "$TMP"
# 新しい設定をテスト、OK なら上書きと reload
mv "$TMP" "$OUT"
nginx -t && nginx -s reload
生成された設定ファイルは http {} ブロックに自動で読み込まれ(include /etc/nginx/conf.d/*.conf 経由)、limit_req_zone で書いた $binary_remote_addr が真の IP になります。crontab に追加:
# 毎週日曜の午前 3 時に同期
0 3 * * 0 /etc/nginx/update-cloudflare-ips.sh >> /var/log/cf-ip-update.log 2>&1
真の IP が効いているかの検証
設定後に最も忘れられがちなのが「実際に検証する」ステップです。最も直接的なやり方は、access log に $remote_addr(上書き後)と $http_cf_connecting_ip(元のヘッダー)を同時に記録し、両者が一致しているか確認することです:
log_format cfdebug '$remote_addr | cf=$http_cf_connecting_ip | xff=$http_x_forwarded_for '
'"$request" $status';
server {
access_log /var/log/nginx/access.log cfdebug;
# ...
}
異なるネットワーク(モバイル 4G、自宅、VPN)からサイトを何度か開き、log を確認します。$remote_addr が cf= と一致し、かつそれが今の自分の IP なら設定は正しいです。$remote_addr がまだ Cloudflare の IP(104.16.x.x、172.64.x.x など)なら、CIDR レンジが網羅されていないかヘッダー名の指定が間違っています。問題ないことを確認したら log_format を通常版に戻せば OK です。
Cloudflare の Rate Limiting と Nginx の役割分担
Cloudflare 自身も Rate Limiting サービスを提供しています(Free プランは制限版、有料プランはフル機能)。CF の制限があるなら Nginx 層は要らないのか?実務的には両方やることをおすすめします。役割分担は大まかにこう:
- Cloudflare 層:大規模 DDoS、ボット、ドメイン全体のクォータをブロック。利点はトラフィックがオリジンに到達しないため、帯域と CPU を節約できる
- Nginx 層:細粒度なエンドポイント別クォータ(ログイン、特定 API、重いエンドポイント)と、Cloudflare がフェイルオープンしたときの最終防衛線
もう一点注意すべきは、Cloudflare はデフォルトでオリジンとの connection coalescing を行うことです。同一 Cloudflare ノードからオリジンへは少数の keep-alive コネクションだけが張られ、すべてのユーザーリクエストがその数本を通って出ていきます。そのため limit_conn でコネクション数を制限している場合、この値はかなり緩めに設定しないと CF 自身をブロックしてしまいます。
モニタリングとデバッグ
設定を本番投入したら放置せず、Nginx の error log を継続的に観察して、ブロックされたリクエストが本当に異常トラフィックか、正規ユーザーを誤って巻き込んでいないかを確認します。limit_req でブロックされたリクエストは error log に次のような行を残します:
2026/04/20 10:23:45 [error] 1234#0: *5678 limiting requests, excess: 5.123 by zone "mylimit", client: 192.0.2.1, server: example.com, request: "GET /api/users HTTP/1.1"
error レベルがうるさいと感じたら、limit_req_log_level で warn や notice に変更できます。また、モニタリングシステム(Prometheus + Grafana、Datadog、クラウド LB のログなど)と組み合わせて 4xx と 5xx の比率を計測することをおすすめします。limit_req_status を 429 にしておけば 429 だけを抽出して Rate Limit の発火頻度を観察できます。
デバッグでよくハマるのは、curl でローカルから Rate Limit をテストすると一向にブロックされない問題です。これはローカル IP の 127.0.0.1 が「本物の」トラフィックとして扱われない、あるいはテスト時のリクエスト数がそもそも閾値に達していないため。ab(Apache Bench)や wrk で負荷テストをかけることをおすすめします:
# 同時 10 コネクション、合計 100 リクエスト送って 503/429 の比率を見る
ab -n 100 -c 10 https://example.com/api/users
よくある落とし穴
最後に、実務でよく踏む落とし穴をいくつか整理します:
- rate を「許容できる最大値」に設定する:rate は「平均速度」であって「上限」ではなく、上限は burst と組み合わせて初めて決まります。「毎秒最大 100 個」にしたいなら
rate=100r/s burst=0にすべきで、rate=100r/s burst=200 nodelayではありません(後者は瞬間的に 201 個通せます) - NAT 環境での誤爆:同じ会社や学校のユーザーは外向き IP を共有することがあり、IP を key にすると全員を 1 ソースとして計算します。ユーザー数が多いなら別の識別子(session、API key)に切り替えるか、rate を緩めます
- 静的リソースの除外を忘れる:Rate Limit をサイトのルートに当てると画像・CSS・JS もクォータに加算され、通常閲覧でブロックされる可能性があります。
locationで静的と動的を分けるか、API やログインなど機微なパスにだけ適用するのがおすすめです - WebSocket と長時間コネクション:
limit_reqはあくまで「リクエスト数」をカウントします。WebSocket の Upgrade 後はもう HTTP リクエストではないので、このルールは個々のメッセージには無効です。WebSocket を保護したいならlimit_connでコネクション数を制限します - reload はカウンタをリセットしない:
nginx -s reloadはホットリロードで、共有メモリ領域の内容は保持されます。rate を調整すると新しいルールはすぐ有効になりますが、既存のバケットの水位は変わりません。完全にリセットしたいならrestartが必要です
Rate Limit は「設定は簡単、チューニングは難しい」機能で、初版を投入した後は実トラフィックを観察してから微調整するのが普通です。緩めの設定(nodelay で大きめの burst)から始めて、誤爆がないことを確認してから徐々に締めていくことをおすすめします。Nginx には limit_req_dry_run モードもあり、「記録のみでブロックしない」運用ができるので、どのリクエストがブロックされるかを先に観察してから本番有効化を判断できます。