前回のAPI ゲートウェイの記事では、入口で回数を数えて、使いすぎた利用者に 429 を返すところを少しだけ試しました。今回は、その回数制限(レート制限)をくわしく見ていきます。
レート制限は、設定は数行でも、「何で数えるか」「どこまでの集中を許すか」「超えたらどう返すか」で動きががらりと変わります。この記事では、お店の入店制限のたとえを使いながら、nginx の公式ドキュメントと Microsoft のドキュメントで確かめ、テスト用の環境で nginx の設定を変えて実際に比べた結果と一緒に、一歩ずつ図にしました。
- レート制限とは何か、なぜ API の入口に必要なのか
- 「何で数えるか(キー)」の選び方と、クォータとの違い
- 固定ウィンドウ・スライディングウィンドウ・トークンバケット・リーキーバケットの違い
- 超えたときの返し方(429 と Retry-After)と、呼ぶ側の正しい待ち方
- nginx での burst・nodelay・dry run の違いと、Azure API Management のポリシー
先に覚えておく3つの言葉
最初に、いちばん大事な3つの言葉を、お店の入店制限にたとえて押さえておきます。
ほかにも、この記事にはいくつか専門の言葉が出てきます。先に、ざっくりした意味を並べておきます。読んでいて迷ったら、ここに戻ってきてください。
| 言葉 | ざっくりした意味 | お店でたとえると |
|---|---|---|
| バースト | 短い間にまとめて届くリクエスト。どこまで許すかを決められる | 一度に来た団体客 |
| クォータ | 1日や1か月など、長い期間に使える総量(割り当て量) | 月に使える回数券 |
| ウィンドウ | 回数を数える時間の枠(1秒、1分など) | 「この1分間に何組」の1分 |
| バケット | 回数をためておく入れ物のたとえ。トークンバケットとリーキーバケットがある | 整理券の箱・行列 |
| バックオフ | 失敗したら、間を空けてから送り直すこと | 混んでいたら時間をずらして出直す |
| ステータスコード | 結果を表す3桁の番号。200 は成功、429 は「多すぎる」 | お店の返事 |
この記事の地図
入口で「誰が・何回」を数え、上限を超えた分は奥に届く前に止めて、待つ秒数を伝えます。
先に全体の地図を見ておくと、迷いにくくなります。
左が API を呼ぶアプリ、右が実際に仕事をする API(バックエンド)です。真ん中のゲートウェイが、キーで相手を見分け、回数を数え、上限と比べます。超えた分は奥に送らずに 429 を返し、何秒待てばよいか(Retry-After)を添えます。呼ぶ側は、その秒数だけ待ってから送り直します。
まずはたとえで:お店の入店制限
レート制限は、API の入口でする「入店制限」です。
人気のお店を思い浮かべてください。入店制限がないと、一度にお客さんが入ってきて店がパンクします。常連さんも初めての人も全員が待たされ、厨房まで止まってしまいます。
入店制限があるお店では、「1分に1組まで」のように決まった速さでだけ入ってもらいます。少しの行列なら外で並んでもらい、あふれたら「5分後にどうぞ」と伝えます。API のレート制限も、まったく同じ考え方です。
なぜレート制限が必要なのか
奥の API と、ほかの利用者を守るためです。
- 奥の API を守る:どんな API にも、さばける量の限界があります。限界を超える前に入口で止めれば、奥まで止まることを防げます。
- 公平にする:1つのアプリが使いすぎても、ほかのアプリが巻き込まれないようにします。
- 費用を抑える:呼んだ回数だけ料金がかかる API や外部サービスを使っているときは、使いすぎがそのまま費用になります。
- 乱用の勢いを落とす:データの大量取得や、パスワードを次々に試すような使い方は、回数を区切るだけで大きく遅くなります。
Microsoft の設計ガイド(Throttling pattern)も、回数を超えた呼び出しは一時的に断り、HTTP 429 で「回数制限で断った」と分かるように返すことを勧めています。
何で数えるか(キー)
「誰ごとに数えるか」を決めるのがキーです。キーを変えると、止められる相手が変わります。
| キー | こんなときに | お店でたとえると |
|---|---|---|
| IP アドレス | サインイン前の画面や、キーを持たない相手 | 入口の顔ぶれで数える |
| API キー・購読 | アプリや契約ごとに上限を決めたい | 会社ごとの予約枠 |
| 利用者 | アプリの中の1人1人を公平に扱いたい | お客さん1人ずつの整理券 |
| API・操作 | 重い処理だけ低い上限にしたい | 人気メニューだけ数量限定 |
IP アドレスで数えるときは注意が要ります。Microsoft のドキュメントは、NAT の裏では多くの利用者が1つの公開 IP アドレスを共有していることが多い、と注意しています。会社のネットワークから使うと、社員全員が「1人」として数えられてしまうことがあります。キーを持つ相手なら、API キーや、確かめたトークンの中の利用者(sub)で数えるほうが公平です。実際には、「アプリごと」と「利用者ごと」のように、いくつか組み合わせることもよくあります。
レート制限とクォータの違い
速さを見るのがレート制限、量を見るのがクォータです。
| 種類 | 見る期間 | 目的 | 例 |
|---|---|---|---|
| レート制限 | 秒・分 | 急な集中(スパイク)を防ぐ | 1秒に1回、1分に100回 |
| クォータ | 日・月 | 使う総量を決める | 1か月に1万回(料金プランごと) |
Microsoft のドキュメントも、レート制限は短く激しい集中から守るためのもの、クォータは1か月の呼び出し回数のように長い期間の量を決めるもの、と分けて説明しています。この記事は、主にレート制限の話です。
4つの数え方(アルゴリズム)
どの方式も、「決まった速さ」と「どこまでの集中を許すか」を決めています。違うのは数え方です。
| 方式 | 数え方 | お店でたとえると |
|---|---|---|
| 固定ウィンドウ | 0:00〜、1:00〜のように時計で区切った枠ごとに数える | 毎時0分に整理券を配り直す |
| スライディングウィンドウ | 「今から見て直近60秒」で数える | 直近1時間に入った人数を数える |
| トークンバケット | 一定の速さで札が増え、1回呼ぶと1枚使う。たまった分はまとめて通る | 整理券が少しずつ補充される箱 |
| リーキーバケット | バケツに入れて、底から一定の速さで流す。あふれた分は断る | 決まった速さで案内する行列 |
Microsoft の設計ガイドは、それぞれの向き不向きを次のようにまとめています。トークンバケットは、決まった大きさまでの集中を許しつつ一定の速さで補充するので、短い集中を吸収したいゲートウェイ向きです。リーキーバケットは一定の速さで流すので、一定の速さで受けたい奥のサーバ向きです。固定ウィンドウは作りが簡単ですが、枠の境目でまとめて通ってしまいます。スライディングウィンドウは、その境目の問題を小さくするかわりに、覚えておく情報が増えます。
固定ウィンドウの境目の問題は、図にすると分かりやすくなります。
上限が「1分に5回」でも、0:59 に5回、1:00 に5回送ると、どちらの枠でも5回以内なので止められません。2秒ほどの間に10回通ってしまいます。
nginx はリーキーバケット
この記事の「試してみる」で使う nginx の limit_req は、公式ドキュメントで「leaky bucket(リーキーバケット)」方式だと説明されています。
- rate:バケツから流れ出る速さ。
rate=1r/sなら1秒に1回です。1秒に1回より少なくしたいときは、30r/m(1分に30回)のように分で書きます。 - burst:バケツの大きさ。速さを超えた分を、いくつまでバケツに入れておけるかです。初期値は0で、少しでも超えたら断ります。
- nodelay:付けないと、バケツに入った分は順番が来るまで待たされます。付けると、バケツに入った分もすぐ奥に通し、バケツの空きだけを数えます。
このほか、途中まではすぐ通し、そこから先は待たせる delay という指定もあります(nginx 1.15.7 から)。
全体の流れ
ここからは、注文 API の前のゲートウェイで、アプリごとに回数を数える例で、流れを順に見ます。準備、上限までのとき、上限を超えたときの3つです。
準備:上限を決めて、呼ぶ側にも伝える
あらすじ:管理者が「奥がさばける量」から上限を決めてゲートウェイに設定し、利用者にも上限と待ち方を伝えます。
| 手順 | 誰が・どこで | 何をするか | 物(設定・上限)はどうなるか |
|---|---|---|---|
| ① | 管理者 | 奥の API がさばける量を調べる | 上限の元になる数字が決まる |
| ② | 管理者 | 数える単位と上限を決める | 例:アプリごとに1秒1回、まとめて3回まで |
| ③ | 管理者 → ゲートウェイ | 設定する | キー・速さ・まとめて許す数が入る |
| ④ | ゲートウェイ | 超えたときの返事を決める | 429 と Retry-After |
| ⑤ | 管理者 → 利用者 | 上限を知らせる | ドキュメントなどに書く |
| ⑥ | 利用者のアプリ | 再送のしかたを組み込む | 429 なら言われた秒数だけ待つ |
上限までのとき:数えるだけで、そのまま通す
あらすじ:リクエストが届くたびに、ゲートウェイは相手を見分けて1回分を数えます。上限の内側なら、そのまま奥に通します。
| 手順 | 誰が・どこで | 何をするか | 回数(バケツ)はどうなるか |
|---|---|---|---|
| ⑦ | アプリ → ゲートウェイ | キーを付けてリクエストを送る | ― |
| ⑧ | ゲートウェイ | キーから誰かを見分ける | app01 のバケツを使うと決まる |
| ⑨ | ゲートウェイ | app01 のバケツを見る | まだ空きがある |
| ⑩ | ゲートウェイ | 1回分を数える | バケツに1つ入る(1秒で流れ出る) |
| ⑪ | ゲートウェイ → API | 転送する | ― |
| ⑫ | バックエンド API | 仕事をする | ― |
| ⑬ | API → ゲートウェイ | レスポンスを返す | ― |
| ⑭ | ゲートウェイ | 記録する | limit=PASSED(通した) |
| ⑮ | ゲートウェイ → アプリ | 200 を返す | ― |
上限を超えたとき:入口で断り、待つ秒数を伝える
あらすじ:バケツがいっぱいのときは、ゲートウェイは奥に送らずに 429 を返します。アプリは言われた秒数だけ待ってから送り直します。
| 手順 | 最初の図では | この図では |
|---|---|---|
| ❶❷ | ⑨ バケツに空きがある | バケツがいっぱい。奥へは送らない |
| ❸ | ⑮ 200 を返す | 429 と Retry-After: 1 を返す |
| ❹ | ⑭ limit=PASSED | limit=REJECTED(断った) |
| ❺❻ | ― | アプリが1秒待ってから送り直す |
| ❼❽ | ⑩〜⑮と同じ | 空きができているので、通って 200 |
❷がポイントです。断るのは入口なので、奥の API は、上限を超えたリクエストを見ることさえありません。Microsoft の設計ガイドも、断る処理そのものが重いと、断るだけで手いっぱいになるので、なるべく早い段階で断るよう勧めています。
超えたときの返し方:429 と Retry-After
「多すぎます(429)」と「◯秒後にどうぞ(Retry-After)」をセットで返します。
429 Too Many Requests は RFC 6585 で決められた番号で、「決まった時間の中で、送ってきたリクエストが多すぎる(レート制限)」ことを表します。RFC 6585 は、429 のレスポンスに、次に送ってよいまでの時間を表す Retry-After ヘッダを付けてもよい、としています。
| 返すもの | 意味 | お店でたとえると |
|---|---|---|
| 429 Too Many Requests | 回数の上限を超えた | 「ただいま満席です」 |
| Retry-After: 1 | 1秒待ってから送り直してほしい(秒数か日時で書く) | 「◯分後にどうぞ」の札 |
| 本文(JSON など) | どの上限を超えたかなどの説明 | 「本日の予約枠は終了しました」 |
Retry-After は HTTP の決まり(RFC 9110)で定められたヘッダで、待つ秒数か、日時のどちらかで書きます。429 のほか、503(一時的に使えない)などでも使われます。Microsoft の設計ガイドは、回数制限で断ったときは 429、予想外の集中でさばけないときは 503 と使い分け、Retry-After を付けて、呼ぶ側が送り直し方を決められるようにすることを勧めています。
なお、IETF では、残りの回数や上限を返す RateLimit・RateLimit-Policy というヘッダが検討されています。2026年11月の時点では、まだ RFC になっていない下書き(Internet-Draft)の段階です。
呼ぶ側の作法:待ってから送り直す
429 を受けたら、すぐ送り直さずに、言われた秒数だけ待ちます。
429 を受けてすぐ送り直すと、また 429 が返ります。サービスによっては、断られたリクエストも回数に数えられるので、いつまでも通りません。みんなが同時にこれをすると、再送の嵐で奥まで重くなります。Microsoft のドキュメントをもとに、正しい待ち方をまとめると次のとおりです。
- Retry-After があれば、その秒数だけ待つ:Microsoft Graph のドキュメントは、これがいちばん早く回復する方法だと説明しています。
- なければ、間をだんだん広げる:1秒、2秒、4秒…のように待ち時間を延ばします(指数バックオフ)。
- 待ち時間を少しずらす:全員が同じ瞬間に戻ってこないように、待ち時間に小さなばらつきを足します。
- 回数に上限を付ける:何度でも送り直さず、数回でやめてエラーにします。
入口が何台もあるとき
「どこで数えるか」で、上限の正確さが変わります。
ゲートウェイを何台も並べると、それぞれの台が自分で数えている場合、同じアプリのリクエストが振り分けられて、合計では上限より多く通ることがあります。Microsoft の設計ガイドは、台ごとの数は速いけれど少なく数えてしまい、共有の置き場(Redis など)で数えるとすべてを数えられるかわりに毎回の判断が少し遅くなる、と説明しています。上限を台数で割っておく方法もあります。どの方式でも、レート制限は「だいたい」で効くものだと考えておくと安全です。
試してみる(nginx で比べる)
ここからは、テスト用の環境で nginx 1.24.0 を動かし、同じ「1秒に1回」の数え方で、超えたときの扱いだけを変えて比べます。奥の API には、前回の記事と同じ Python の小さなサーバ(backend.py)を使います。
キーの値(demo-key-app01 など)は、この試験のために作った見本の値です。
設定を書いて動かす(図10の②〜④)
確かめること:同じ数え方のまま、場所(パス)ごとに超えたときの扱いを変えられること。
やっていること:お店の入口に、4種類の入店ルールを貼り出します。
ratelimit.conf(入口のルール)
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {}
http {
log_format rl '$time_iso8601 client=$client_id "$request" '
'$status limit=$limit_req_status rt=$request_time';
access_log logs/access.log rl;
map $http_x_api_key $client_id {
default "";
"demo-key-app01" "app01";
"demo-key-app02" "app02";
}
limit_req_zone $client_id zone=perclient:1m rate=1r/s;
limit_req_status 429;
server {
listen 127.0.0.1:8080;
default_type application/json;
error_page 429 = @too_many;
location /strict/ {
limit_req zone=perclient;
proxy_pass http://127.0.0.1:9001/;
}
location /queue/ {
limit_req zone=perclient burst=3;
proxy_pass http://127.0.0.1:9001/;
}
location /nodelay/ {
limit_req zone=perclient burst=3 nodelay;
proxy_pass http://127.0.0.1:9001/;
}
location /dryrun/ {
limit_req zone=perclient;
limit_req_dry_run on;
proxy_pass http://127.0.0.1:9001/;
}
location @too_many {
add_header Retry-After 1 always;
return 429 '{"error":"too many requests"}\n';
}
}
}
上から順に、かみ砕くとこうなります。
log_format rl:記録の形を決める。$limit_req_statusに、通した(PASSED)・待たせた(DELAYED)・断った(REJECTED)などの結果が入る(nginx 1.17.6 から)map:X-API-Key ヘッダの値から、誰か(app01・app02)を決める。一覧にないキーは空にするlimit_req_zone:「誰か」ごとに、1秒1回の速さで数える入れ物(perclient)を作るlimit_req_status 429:断るときの番号を 429 にする。nginx の初期値は 503/strict/・/queue/・/nodelay/・/dryrun/:同じ入れ物を使い、超えたときの扱いだけを変えた4つの場所@too_many:断ったときの返事。429 にRetry-After: 1を付ける。nginx の limit_req は Retry-After を付けないので、ここで足している(テスト用の環境で、この行を外すと付かないことも確かめた)
奥の API(注文 API、9001番)を動かす
python3 backend.py orders 9001 &
設定の書き方が正しいか確かめる
nginx -t -p /srv/rl/ -c ratelimit.conf
nginx: the configuration file /srv/rl/ratelimit.conf syntax is ok nginx: configuration file /srv/rl/ratelimit.conf test is successful
ゲートウェイを動かす(8080番で待ち受ける)
nginx -p /srv/rl/ -c ratelimit.conf
結果の読み方:「syntax is ok」「test is successful」は、書き方に誤りがないという意味です。入店ルールの貼り出しが終わりました。
余裕なしで数える(図12の❶〜❸)
確かめること:burst を付けないと、1秒に1回を少しでも超えたらすぐ断られること。
やっていること:行列を一切認めないお店に、続けて3回入ろうとし、1秒後にもう一度入ろうとします。
/strict/ を続けて3回呼び、1秒待ってからもう1回呼ぶ
for i in 1 2 3; do
curl -s -o /dev/null -w "%{http_code} " -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/strict/list
done; echo
sleep 1
curl -s -o /dev/null -w "%{http_code}\n" -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/strict/list
200 429 429
200
エラーログで、断った記録を見る
grep "limiting requests" logs/error.log | tail -n 1
2026/11/02 15:19:11 [error] 261#261: *4 limiting requests, excess: 0.982 by zone "perclient", client: 127.0.0.1, server: , request: "GET /strict/list HTTP/1.1", host: "127.0.0.1:8080"
結果の読み方:
- 1回目は 200、すぐ後の2回目と3回目は 429 でした。1秒に1回の速さを、少しでも超えたからです。
- 1秒待った4回目は 200 でした。バケツから1回分が流れ出て、空きができたからです。
- エラーログの
limiting requestsが、断った記録です。excess: 0.982は速さを超えた分で、1秒たたないうちに次が来たので、ほぼ1回分超えていた、と読めます。
少しの連続を並ばせる(burst)
確かめること:burst を付けると、バケツに入った分は断られずに、順番に待たされてから通ること。
やっていること:「3組までなら外で並んでいい」お店に、5組が同時に来ます。
/queue/ に5つ同時に送る(番号と、かかった秒数を表示)
sleep 5
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/queue/list &
done; wait
200 0.004692
429 0.000520
200 1.002531
200 1.997147
200 3.001430
アクセスログの最後の5行を見る
tail -n 5 logs/access.log
2026-11-02T15:19:17+00:00 client=app01 "GET /queue/list HTTP/1.1" 200 limit=PASSED rt=0.001 2026-11-02T15:19:17+00:00 client=app01 "GET /queue/list HTTP/1.1" 429 limit=REJECTED rt=0.000 2026-11-02T15:19:18+00:00 client=app01 "GET /queue/list HTTP/1.1" 200 limit=DELAYED rt=1.001 2026-11-02T15:19:19+00:00 client=app01 "GET /queue/list HTTP/1.1" 200 limit=DELAYED rt=1.996 2026-11-02T15:19:20+00:00 client=app01 "GET /queue/list HTTP/1.1" 200 limit=DELAYED rt=2.997
結果の読み方:
- 1つはすぐ 200、1つはすぐ 429 でした。残りの3つは、約1秒・約2秒・約3秒かかってから 200 でした。
- 速さ(1秒1回)を超えた4つのうち、3つはバケツ(burst=3)に入って待たされ、入りきらなかった1つが断られた、ということです。
- ログの
limit=DELAYEDが待たされた分、limit=REJECTEDが断られた分です。待たされた分はrt=(かかった時間)にも、その待ち時間が出ています。
奥の API から見ると、1秒に1回のきれいな速さで届いています。そのかわり、呼ぶ側は最大3秒待たされます。
並ばせずに通す(burst + nodelay)
確かめること:nodelay を付けると、バケツに入る分はすぐ通り、空きが1秒に1つずつ戻ること。
やっていること:「3組までなら待たずに入っていい。ただし空きは1分に1組ずつしか戻らない」お店に、5組が同時に来ます。
/nodelay/ に5つ同時に送り、1秒後に2回続けて送る
sleep 5
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/nodelay/list &
done; wait
sleep 1
for i in 1 2; do
curl -s -o /dev/null -w "%{http_code} " -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/nodelay/list
done; echo
200 0.000891 200 0.004871 200 0.000773 200 0.004141 429 0.001589 200 429
結果の読み方:
- 5つ同時のうち、4つはすぐ 200(どれも 0.005 秒以内)、1つは 429 でした。STEP 3 と違って、待たされた分はありません。
- 1秒後の2回は、1回目が 200、2回目が 429 でした。1秒で空きが1つだけ戻ったからです。
API の入口では、呼ぶ側を待たせずに済む nodelay がよく使われます。ただし、奥には短い間にまとめて届くので、奥がその集中に耐えられるかを確かめておきます。
Retry-After を返し、守って送り直す(図12の❸〜❽)
確かめること:429 に Retry-After が付いて返り、それを守って待てば、送り直しが通ること。
やっていること:「1秒後にどうぞ」の札を受け取り、その札どおりに出直すお客さんを用意します。
続けて2回呼び、2回目のレスポンスのヘッダを見る
sleep 5 curl -s -o /dev/null -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/strict/list curl -s -i -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/strict/list
HTTP/1.1 429 Too Many Requests Server: nginx/1.24.0 (Ubuntu) Date: Mon, 02 Nov 2026 15:19:31 GMT Content-Type: application/json Content-Length: 30 Connection: keep-alive Retry-After: 1 {"error":"too many requests"}
client.py(429 なら Retry-After の秒数だけ待って送り直す、呼ぶ側の見本)
import time, urllib.request, urllib.error
URL = "http://127.0.0.1:8080/strict/list"
KEY = {"X-API-Key": "demo-key-app01"}
start = time.monotonic()
def now():
return f"{time.monotonic() - start:4.1f}s"
for n in range(1, 4):
for attempt in range(1, 4):
try:
with urllib.request.urlopen(urllib.request.Request(URL, headers=KEY)) as r:
print(now(), f"req{n}", r.status)
break
except urllib.error.HTTPError as e:
wait = int(e.headers.get("Retry-After", "1"))
print(now(), f"req{n}", e.code, f"Retry-After={wait} -> wait")
time.sleep(wait)
client.py を動かす
sleep 5 python3 client.py
0.0s req1 200 0.0s req2 429 Retry-After=1 -> wait 1.0s req2 200 1.0s req3 429 Retry-After=1 -> wait 2.0s req3 200
結果の読み方:
- ヘッダに
HTTP/1.1 429 Too Many RequestsとRetry-After: 1が並んでいます。「多すぎるので、1秒後にどうぞ」という返事です。 - client.py は、req2 と req3 で 429 を受け、どちらも1秒待ってから送り直して 200 になりました。3回とも、最後は成功しています。
- client.py は、1つのリクエストを3回まで試してやめる作りにしています(
range(1, 4))。送り直しの回数に上限を付けるためです。
本番の前に数えるだけ(dry run)
確かめること:dry run では、断らずに通しながら、「本番なら断っていた」分を記録できること。
やっていること:入店制限を始める前に、入口で人数だけ数えて、「制限していたら何組を断っていたか」を記録します。
/dryrun/ を続けて3回呼ぶ
sleep 5
for i in 1 2 3; do
curl -s -o /dev/null -w "%{http_code} " -H "X-API-Key: demo-key-app01" http://127.0.0.1:8080/dryrun/list
done; echo
200 200 200
アクセスログの最後の3行を見る
tail -n 3 logs/access.log
2026-11-02T15:19:44+00:00 client=app01 "GET /dryrun/list HTTP/1.1" 200 limit=PASSED rt=0.001 2026-11-02T15:19:44+00:00 client=app01 "GET /dryrun/list HTTP/1.1" 200 limit=REJECTED_DRY_RUN rt=0.002 2026-11-02T15:19:44+00:00 client=app01 "GET /dryrun/list HTTP/1.1" 200 limit=REJECTED_DRY_RUN rt=0.001
エラーログで、dry run の記録を見る
grep "dry run" logs/error.log | tail -n 1
2026/11/02 15:19:44 [error] 261#261: *43 limiting requests, dry run, excess: 0.982 by zone "perclient", client: 127.0.0.1, server: , request: "GET /dryrun/list HTTP/1.1", host: "127.0.0.1:8080"
結果の読み方:返事は3回とも 200 で、利用者には何も起きていません。それでもログには、2回目と3回目が REJECTED_DRY_RUN(本番なら断っていた)と残り、エラーログにも dry run と記録されています。limit_req_dry_run は nginx 1.17.1 からの機能です。上限を決めたら、まず dry run で数日ようすを見て、正しい利用者が止まらないかを確かめてから本番にすると安全です。
ここからは Microsoft の場合です
Azure API Management の回数のポリシー
速さは rate-limit 系、総量は quota 系のポリシーで決めます。
前回の記事で見た Azure API Management(APIM)では、回数の上限をポリシーで設定します。
| ポリシー | 何を数えるか | 超えたとき |
|---|---|---|
rate-limit | 購読(サブスクリプション)ごとの、短い時間の回数 | 429 |
rate-limit-by-key | 自由に決めたキーごとの、短い時間の回数 | 429 |
quota | 購読ごとの、長い期間の回数や通信量 | 403 |
quota-by-key | 自由に決めたキーごとの、長い期間の回数や通信量 | 403 |
どのポリシーも、超えたときに待つ秒数の目安を Retry-After ヘッダで返します。rate-limit 系の renewal-period(数える時間の長さ)は最大300秒です。remaining-calls-header-name を指定すると、残りの回数をレスポンスのヘッダで返すこともできます。rate-limit-by-key と quota-by-key は、Consumption など一部の階層では使えません。
キーの決め方の例
IP アドレスごとに、1分に10回まで・1か月の総量も決める例(Microsoft Learn より)
<rate-limit-by-key calls="10"
renewal-period="60"
counter-key="@(context.Request.IpAddress)" />
<quota-by-key calls="1000000"
bandwidth="10000"
renewal-period="2629800"
counter-key="@(context.Request.IpAddress)" />
上の rate-limit-by-key が「1分(60秒)に10回まで」の速さ、下の quota-by-key が「1か月に100万回・10,000KB まで」の総量です。どちらも counter-key にクライアントの IP アドレスを入れています。Microsoft は、NAT の裏の利用者がまとめて数えられることがあるものの、サインイン前の API では IP アドレスがいちばんよい選択になることもある、と説明しています。
トークンの中の利用者(sub)ごとに数える例(Microsoft Learn より)
<rate-limit-by-key calls="10"
renewal-period="60"
counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Subject)" />
Authorization ヘッダのトークンを JWT として読み、その中の sub(誰か)をキーにしています。サインインした利用者1人1人を、公平に数えられます。
階層によって数え方が違う
APIM の回数制限は、階層によって数え方(アルゴリズム)が違います。クラシックの階層はスライディングウィンドウ、v2 の階層はトークンバケットです。
60秒に6回の例(Microsoft Learn より)
<rate-limit-by-key calls="6" renewal-period="60" counter-key="Counter1"/>
Microsoft のドキュメントによると、v2 の階層では、この設定はバケツの大きさが6回のトークンバケットになります。最初に6回までまとめて通り、そのあとは60秒に6回(1秒に0.1回)の速さで札が補充されます。
また、APIM のドキュメントは、回数制限は分散したしくみで数えるので決して完全には正確にならないこと、複数のリージョンにゲートウェイを置くとリージョンごとに別々に数えることを注意しています。前回の記事で見たとおり、ポリシーで断られたリクエストも回数に数えられます。
ほかの Microsoft のサービスも、同じ考え方で回数を区切っています。たとえば Microsoft Graph は、回数を超えると 429 と Retry-After を返し、呼ぶ側にはその秒数だけ待ってから送り直すよう案内しています。
よくある間違い
| 間違い | どうするか |
|---|---|
| キーのない相手を数えていない | キーが空なら先に 401 で断るか、IP など別のキーでも数える |
| 全員を IP アドレスだけで数える | キーやトークンの利用者で数え、IP はサインイン前などに使う |
| 上限を勘で決める | 奥がさばける量から決め、dry run で止まる量を確かめる |
| 429 だけ返して、待つ秒数を伝えない | Retry-After を付ける。上限を利用者に知らせる |
| 呼ぶ側が 429 ですぐ送り直す | Retry-After だけ待つ。なければ間を広げ、回数に上限を付ける |
| レート制限だけで、使う総量を決めていない | 料金や契約にかかわる量はクォータで決める |
うまくいかないとき
レート制限がうまくいかないときは、まず「数えているキー」と、記録に残った結果(nginx なら limit= の値)を確かめます。
テスト用の環境で、キーを付けずに /strict/ を続けて呼んでみました。
キーを付けずに /strict/ を5回続けて呼ぶ
sleep 5
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "%{http_code} " http://127.0.0.1:8080/strict/list
done; echo
200 200 200 200 200
アクセスログの最後の5行を見る
tail -n 5 logs/access.log
2026-11-02T15:19:49+00:00 client= "GET /strict/list HTTP/1.1" 200 limit=- rt=0.002 2026-11-02T15:19:49+00:00 client= "GET /strict/list HTTP/1.1" 200 limit=- rt=0.000 2026-11-02T15:19:49+00:00 client= "GET /strict/list HTTP/1.1" 200 limit=- rt=0.001 2026-11-02T15:19:49+00:00 client= "GET /strict/list HTTP/1.1" 200 limit=- rt=0.001 2026-11-02T15:19:49+00:00 client= "GET /strict/list HTTP/1.1" 200 limit=- rt=0.000
結果の読み方:STEP 2 では2回目から断られたのに、キーなしでは5回とも 200 でした。ログの client= が空で、limit=-(数えていない)になっています。この設定では、一覧にないキーは空になり、nginx のドキュメントのとおり、キーが空のリクエストは数えられません。前回の記事のように、キーがないリクエストは先に 401 で断っておく必要があります。
| 困っていること | よくある原因 | 確認すること |
|---|---|---|
| 制限がかからない | キーが空で数えていない。dry run のまま | ログの client= と limit= の値。limit_req_dry_run の設定 |
| 正しい利用者まで止まる | IP で数えている。上限が低すぎる | 数えているキー。dry run の記録で、止まる量 |
| 429 ではなく 503 が返る | nginx の limit_req_status が初期値(503) | limit_req_status 429 の設定 |
| エラーではなく遅くなる | burst だけで nodelay がない | ログの limit=DELAYED と rt= の値 |
| 429 がいつまでも続く | 呼ぶ側が待たずに送り直している | Retry-After を守っているか。再送の回数 |
| 台数を増やしたら緩くなった | 台ごとに数えている | 数える場所(台ごとか、共有か) |
まとめ
| 項目 | 内容 | お店でたとえると |
|---|---|---|
| レート制限 | 決まった時間に通す回数の上限。入口で数えて止める | 入店制限 |
| キー | 誰ごとに数えるか。API キー・利用者・IP・操作 | 整理券の名前 |
| クォータ | 日や月の総量。レート制限とは別に決める | 月の回数券 |
| 数え方 | 固定・スライディング・トークンバケット・リーキーバケット | 整理券の配り方 |
| 超えたとき | 429 と Retry-After。奥には届かない | 「◯分後にどうぞ」の札 |
| 呼ぶ側 | 言われた秒数だけ待つ。間を広げ、回数に上限を付ける | 時間をずらして出直す |
| nginx | rate・burst・nodelay・dry run で調整 | 速さ・行列の長さ・待たせ方・お試し期間 |
| Azure | APIM の rate-limit 系(429)と quota 系(403) | 入店制限と回数券 |
参考にした資料
この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- RFC 6585: Additional HTTP Status Codes(IETF):429 Too Many Requests の意味と Retry-After
- Retry-After header(MDN):RFC 9110 での Retry-After の書き方(秒数か日時)と、429・503 での使われ方
- RateLimit header fields for HTTP(IETF Datatracker):RateLimit・RateLimit-Policy ヘッダが Internet-Draft の段階であること
- Module ngx_http_limit_req_module(nginx):リーキーバケット方式、rate・burst・nodelay・delay、空のキーは数えないこと、limit_req_status の初期値、limit_req_dry_run、$limit_req_status の値
- Throttling pattern・Rate Limiting pattern(Microsoft Learn):4つの方式の向き不向き、429 と 503 の使い分けと Retry-After、早い段階で断ること、台ごとの数と共有の置き場、ばらつきを付けた回数の少ない再送
- Advanced request throttling with Azure API Management(Microsoft Learn):レート制限とクォータの違い、クラシックと v2 の数え方、60秒に6回の例、IP アドレスと利用者で数える例、NAT の注意、リージョンごとの数、正確さの注意
- Limit call rate by subscription・Limit call rate by key・Set usage quota by subscription・Set usage quota by key(Microsoft Learn):429 と 403、Retry-After、renewal-period の最大値、残りの回数を返すヘッダ、使える階層
- Microsoft Graph throttling guidance(Microsoft Learn):429 と Retry-After を守って送り直すこと、Retry-After がないときの指数バックオフ
あとがき
nginx で burst と nodelay を付け替えて比べてみると、同じ「1秒に1回」でも、呼ぶ側から見た動きがまったく違うのがよく分かりました。burst だけだと 429 は減るものの、そのぶん3秒も待たされるリクエストが出ます。もう1つ印象に残ったのは、キーを付けないリクエストが1回も数えられずに素通りしたことです。設定を書いたら、キーがないとき・空のときにどう数えられるかまで、ログで確かめておきたいと思いました。次の記事では、入口でリクエストの中身を見て止める WAF を見ていきます。
コメント