【図解】レート制限とは?APIの使いすぎを入口で止めるしくみと429・Retry-Afterを初心者向けに解説

前回のAPI ゲートウェイの記事では、入口で回数を数えて、使いすぎた利用者に 429 を返すところを少しだけ試しました。今回は、その回数制限(レート制限)をくわしく見ていきます。

レート制限は、設定は数行でも、「何で数えるか」「どこまでの集中を許すか」「超えたらどう返すか」で動きががらりと変わります。この記事では、お店の入店制限のたとえを使いながら、nginx の公式ドキュメントと Microsoft のドキュメントで確かめ、テスト用の環境で nginx の設定を変えて実際に比べた結果と一緒に、一歩ずつ図にしました。

この記事でわかること
  1. レート制限とは何か、なぜ API の入口に必要なのか
  2. 「何で数えるか(キー)」の選び方と、クォータとの違い
  3. 固定ウィンドウ・スライディングウィンドウ・トークンバケット・リーキーバケットの違い
  4. 超えたときの返し方(429 と Retry-After)と、呼ぶ側の正しい待ち方
  5. nginx での burst・nodelay・dry run の違いと、Azure API Management のポリシー

先に覚えておく3つの言葉

最初に、いちばん大事な3つの言葉を、お店の入店制限にたとえて押さえておきます。

レート制限たとえ:入店制限決まった時間に通す回数の上限。超えた分は断るか、待たせるキー(数える単位)たとえ:整理券の名前誰ごとに回数を数えるか。API キー・利用者・IP など429 と Retry-Afterたとえ:「◯秒後にどうぞ」の札回数を超えたときの返事と、何秒待てばよいかの目安この3つが分かれば、この記事の話はほぼ追える
図1 先に覚えておく3つの言葉

ほかにも、この記事にはいくつか専門の言葉が出てきます。先に、ざっくりした意味を並べておきます。読んでいて迷ったら、ここに戻ってきてください。

言葉ざっくりした意味お店でたとえると
バースト短い間にまとめて届くリクエスト。どこまで許すかを決められる一度に来た団体客
クォータ1日や1か月など、長い期間に使える総量(割り当て量)月に使える回数券
ウィンドウ回数を数える時間の枠(1秒、1分など)「この1分間に何組」の1分
バケット回数をためておく入れ物のたとえ。トークンバケットとリーキーバケットがある整理券の箱・行列
バックオフ失敗したら、間を空けてから送り直すこと混んでいたら時間をずらして出直す
ステータスコード結果を表す3桁の番号。200 は成功、429 は「多すぎる」お店の返事

この記事の地図

ひとことで言うと

入口で「誰が・何回」を数え、上限を超えた分は奥に届く前に止めて、待つ秒数を伝えます。

先に全体の地図を見ておくと、迷いにくくなります。

呼ぶ側アプリ app01アプリ app02API ゲートウェイ① 誰かを見分ける(キー)② 回数を数える③ 上限と比べる④ 超えたら 429 を返す奥上限までバックエンド API429 + Retry-After: 1呼ぶ側は、言われた秒数だけ待ってから送り直す入口で数えて、上限を超えた分は奥に届く前に止める
図2 この記事の地図

左が API を呼ぶアプリ、右が実際に仕事をする API(バックエンド)です。真ん中のゲートウェイが、キーで相手を見分け、回数を数え、上限と比べます。超えた分は奥に送らずに 429 を返し、何秒待てばよいか(Retry-After)を添えます。呼ぶ側は、その秒数だけ待ってから送り直します。

まずはたとえで:お店の入店制限

ひとことで言うと

レート制限は、API の入口でする「入店制限」です。

人気のお店を思い浮かべてください。入店制限がないと、一度にお客さんが入ってきて店がパンクします。常連さんも初めての人も全員が待たされ、厨房まで止まってしまいます。

入店制限なし店内一度に入ってきて店がパンク常連も一見さんも全員が待たされる厨房(奥)まで止まってしまう入店制限あり店内1分に1組まで「5分後にどうぞ」決まった速さでだけ入ってもらう少しの行列なら外で並んでもらうあふれたら「◯分後に」と伝えるレート制限は、API の入口でする「入店制限」
図3 たとえで見るレート制限

入店制限があるお店では、「1分に1組まで」のように決まった速さでだけ入ってもらいます。少しの行列なら外で並んでもらい、あふれたら「5分後にどうぞ」と伝えます。API のレート制限も、まったく同じ考え方です。

なぜレート制限が必要なのか

ひとことで言うと

奥の API と、ほかの利用者を守るためです。

1奥の API を守るさばける量を超えないようにするたとえ:厨房が回る人数だけ入れる2公平にする1人の使いすぎで、ほかの人を止めないたとえ:1組の大人数で満席にしない3費用を抑える従量課金の API や外部サービスの使いすぎを防ぐたとえ:食材の使いすぎを防ぐ4乱用の勢いを落とす大量取得やパスワードの総当たりを遅くするたとえ:居座り・買い占めを防ぐ回数を区切ると、奥の API も、ほかの利用者も守れる
図4 レート制限が必要な4つの理由
  • 奥の API を守る:どんな API にも、さばける量の限界があります。限界を超える前に入口で止めれば、奥まで止まることを防げます。
  • 公平にする:1つのアプリが使いすぎても、ほかのアプリが巻き込まれないようにします。
  • 費用を抑える:呼んだ回数だけ料金がかかる API や外部サービスを使っているときは、使いすぎがそのまま費用になります。
  • 乱用の勢いを落とす:データの大量取得や、パスワードを次々に試すような使い方は、回数を区切るだけで大きく遅くなります。

Microsoft の設計ガイド(Throttling pattern)も、回数を超えた呼び出しは一時的に断り、HTTP 429 で「回数制限で断った」と分かるように返すことを勧めています。

何で数えるか(キー)

ひとことで言うと

「誰ごとに数えるか」を決めるのがキーです。キーを変えると、止められる相手が変わります。

数える単位(キー)よいところ気をつけることIP アドレスキーを持たない相手にも使えるNAT の裏の大勢が1人に見えるAPI キー・購読アプリ(契約)ごとに数えられるアプリの中の1人の使いすぎは分からない利用者(トークンの sub)人ごとに公平に数えられるサインインした相手にしか使えないAPI・操作ごと重い API だけ低い上限にできるほかのキーと組み合わせて使う「誰ごとに数えるか」で、止められる相手が変わる
図5 何で数えるか(キー)の比較
キーこんなときにお店でたとえると
IP アドレスサインイン前の画面や、キーを持たない相手入口の顔ぶれで数える
API キー・購読アプリや契約ごとに上限を決めたい会社ごとの予約枠
利用者アプリの中の1人1人を公平に扱いたいお客さん1人ずつの整理券
API・操作重い処理だけ低い上限にしたい人気メニューだけ数量限定

IP アドレスで数えるときは注意が要ります。Microsoft のドキュメントは、NAT の裏では多くの利用者が1つの公開 IP アドレスを共有していることが多い、と注意しています。会社のネットワークから使うと、社員全員が「1人」として数えられてしまうことがあります。キーを持つ相手なら、API キーや、確かめたトークンの中の利用者(sub)で数えるほうが公平です。実際には、「アプリごと」と「利用者ごと」のように、いくつか組み合わせることもよくあります。

レート制限とクォータの違い

ひとことで言うと

速さを見るのがレート制限、量を見るのがクォータです。

レート制限(短い時間)上限1秒ごとの回数急な集中(スパイク)を防ぐ例:1秒に1回、1分に100回超えたら 429クォータ(長い期間)今月 7,200 回上限 10,000 回月の初めに0に戻る使う総量を決める例:1か月に1万回(料金プランごと)超えたら、期間が変わるまで使えない速さを見るのがレート制限、量を見るのがクォータ
図6 レート制限とクォータの違い
種類見る期間目的例
レート制限秒・分急な集中(スパイク)を防ぐ1秒に1回、1分に100回
クォータ日・月使う総量を決める1か月に1万回(料金プランごと)

Microsoft のドキュメントも、レート制限は短く激しい集中から守るためのもの、クォータは1か月の呼び出し回数のように長い期間の量を決めるもの、と分けて説明しています。この記事は、主にレート制限の話です。

4つの数え方(アルゴリズム)

ひとことで言うと

どの方式も、「決まった速さ」と「どこまでの集中を許すか」を決めています。違うのは数え方です。

固定ウィンドウ0:00〜1:00〜2:00〜時計で区切った枠ごとに数える枠が変わると0から数え直す作りは簡単。ただし枠の境目に続けて送ると、短い間に倍まで通るスライディングウィンドウ直近の60秒「今から見て直近◯秒」で数える枠が時間とともに滑っていく境目の問題が小さくなるかわりに覚えておく情報が増えるトークンバケット一定の速さで札が増える1回呼ぶと札を1枚使うたまった札の分だけ、まとめて通る札がなくなったら断る短い集中を吸収したい入口向きリーキーバケット上から入れて、底の穴から一定の速さで流れ出る決まった速さで奥へ流すバケツからあふれた分は断る一定の速さで受けたい奥向き(nginx)どの方式も「決まった速さ」と「どこまでの集中を許すか」を決めている
図7 レート制限の4つの方式
方式数え方お店でたとえると
固定ウィンドウ0:00〜、1:00〜のように時計で区切った枠ごとに数える毎時0分に整理券を配り直す
スライディングウィンドウ「今から見て直近60秒」で数える直近1時間に入った人数を数える
トークンバケット一定の速さで札が増え、1回呼ぶと1枚使う。たまった分はまとめて通る整理券が少しずつ補充される箱
リーキーバケットバケツに入れて、底から一定の速さで流す。あふれた分は断る決まった速さで案内する行列

Microsoft の設計ガイドは、それぞれの向き不向きを次のようにまとめています。トークンバケットは、決まった大きさまでの集中を許しつつ一定の速さで補充するので、短い集中を吸収したいゲートウェイ向きです。リーキーバケットは一定の速さで流すので、一定の速さで受けたい奥のサーバ向きです。固定ウィンドウは作りが簡単ですが、枠の境目でまとめて通ってしまいます。スライディングウィンドウは、その境目の問題を小さくするかわりに、覚えておく情報が増えます。

固定ウィンドウの境目の問題は、図にすると分かりやすくなります。

0:001:002:001つめの枠(上限5回)2つめの枠(上限5回)2秒ほどの間に10回通る0:591:00どちらの枠でも5回以内なので、固定ウィンドウでは止められない
図8 固定ウィンドウの境目の問題

上限が「1分に5回」でも、0:59 に5回、1:00 に5回送ると、どちらの枠でも5回以内なので止められません。2秒ほどの間に10回通ってしまいます。

nginx はリーキーバケット

この記事の「試してみる」で使う nginx の limit_req は、公式ドキュメントで「leaky bucket(リーキーバケット)」方式だと説明されています。

届いたリクエストapp01 から続けてburst(並べる数)rate:1秒に1回ずつ流れ出るあふれた分429 で断る奥の API へ決まった速さで届くnodelay なし:バケツに入った分は、順番が来るまで待たされてから奥へ届くnodelay あり:バケツに入った分もすぐ奥へ通し、バケツの空きだけを数えるnginx は「1秒に何回」と「何回までバケツに入れるか」で制限する
図9 nginx のリーキーバケットのしくみ
  • rate:バケツから流れ出る速さ。rate=1r/s なら1秒に1回です。1秒に1回より少なくしたいときは、30r/m(1分に30回)のように分で書きます。
  • burst:バケツの大きさ。速さを超えた分を、いくつまでバケツに入れておけるかです。初期値は0で、少しでも超えたら断ります。
  • nodelay:付けないと、バケツに入った分は順番が来るまで待たされます。付けると、バケツに入った分もすぐ奥に通し、バケツの空きだけを数えます。

このほか、途中まではすぐ通し、そこから先は待たせる delay という指定もあります(nginx 1.15.7 から)。

全体の流れ

ここからは、注文 API の前のゲートウェイで、アプリごとに回数を数える例で、流れを順に見ます。準備、上限までのとき、上限を超えたときの3つです。

準備:上限を決めて、呼ぶ側にも伝える

あらすじ:管理者が「奥がさばける量」から上限を決めてゲートウェイに設定し、利用者にも上限と待ち方を伝えます。

管理者APIゲートウェイ利用者のアプリ① 奥の API がさばける量を調べる② 数える単位と上限を決める例:アプリごとに1秒1回③ 設定するキー・速さ・まとめて許す数④ 超えたときの返事を決める429 + Retry-After⑤ 上限を知らせるドキュメントなどで⑥ 再送のしかたを組み込む429 なら言われた秒数だけ待つ上限は「奥がさばける量」から決め、呼ぶ側にも伝えておく
図10 準備の流れ(①〜⑥)
手順誰が・どこで何をするか物(設定・上限)はどうなるか
①管理者奥の API がさばける量を調べる上限の元になる数字が決まる
②管理者数える単位と上限を決める例:アプリごとに1秒1回、まとめて3回まで
③管理者 → ゲートウェイ設定するキー・速さ・まとめて許す数が入る
④ゲートウェイ超えたときの返事を決める429 と Retry-After
⑤管理者 → 利用者上限を知らせるドキュメントなどに書く
⑥利用者のアプリ再送のしかたを組み込む429 なら言われた秒数だけ待つ

上限までのとき:数えるだけで、そのまま通す

あらすじ:リクエストが届くたびに、ゲートウェイは相手を見分けて1回分を数えます。上限の内側なら、そのまま奥に通します。

利用者のアプリAPIゲートウェイバックエンドAPI⑦ GET /orders/listX-API-Key を付ける⑧ 誰かを見分けるキー → app01⑨ app01 のバケツを見るまだ空きがある⑩ 1回分を数えるバケツに1つ入れる⑪ 転送⑫ 仕事をする⑬ レスポンス⑭ 記録するlimit=PASSED⑮ 200上限までは、数えるだけでそのまま奥に通す
図11 上限までのときの流れ(⑦〜⑮)
手順誰が・どこで何をするか回数(バケツ)はどうなるか
⑦アプリ → ゲートウェイキーを付けてリクエストを送る―
⑧ゲートウェイキーから誰かを見分けるapp01 のバケツを使うと決まる
⑨ゲートウェイapp01 のバケツを見るまだ空きがある
⑩ゲートウェイ1回分を数えるバケツに1つ入る(1秒で流れ出る)
⑪ゲートウェイ → API転送する―
⑫バックエンド API仕事をする―
⑬API → ゲートウェイレスポンスを返す―
⑭ゲートウェイ記録するlimit=PASSED(通した)
⑮ゲートウェイ → アプリ200 を返す―

上限を超えたとき:入口で断り、待つ秒数を伝える

あらすじ:バケツがいっぱいのときは、ゲートウェイは奥に送らずに 429 を返します。アプリは言われた秒数だけ待ってから送り直します。

利用者のアプリAPIゲートウェイバックエンドAPI❶ 短い間に続けて送る❷ バケツがいっぱい奥へは送らない奥には届かない❸ 429Retry-After: 1❹ 記録するlimit=REJECTED❺ 言われた秒数(1秒)待つ❻ 送り直す❼ 空きができている数えて奥へ(⑩〜⑬)転送❽ 200断るのは入口。待ってから送り直せば、正しく通る
図12 上限を超えたときの流れ(❶〜❽)
手順最初の図ではこの図では
❶❷⑨ バケツに空きがあるバケツがいっぱい。奥へは送らない
❸⑮ 200 を返す429 と Retry-After: 1 を返す
❹⑭ limit=PASSEDlimit=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: 11秒待ってから送り直してほしい(秒数か日時で書く)「◯分後にどうぞ」の札
本文(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送る429送る429送る429送る429断られた分も回数に数えられ、いつまでも通らないみんなが同時にこれをすると、再送の嵐で、奥まで重くなる待ってから送り直す1Retry-After があればその秒数だけ待つ2なければ1秒・2秒・4秒…と間を広げる3待ち時間を少しずらす全員が同じ瞬間に戻らない4回数に上限を付ける数回でやめて、エラーにする429 を受けたら、言われたとおりに待つのがいちばんの近道
図13 呼ぶ側の作法

429 を受けてすぐ送り直すと、また 429 が返ります。サービスによっては、断られたリクエストも回数に数えられるので、いつまでも通りません。みんなが同時にこれをすると、再送の嵐で奥まで重くなります。Microsoft のドキュメントをもとに、正しい待ち方をまとめると次のとおりです。

  1. Retry-After があれば、その秒数だけ待つ:Microsoft Graph のドキュメントは、これがいちばん早く回復する方法だと説明しています。
  2. なければ、間をだんだん広げる:1秒、2秒、4秒…のように待ち時間を延ばします(指数バックオフ)。
  3. 待ち時間を少しずらす:全員が同じ瞬間に戻ってこないように、待ち時間に小さなばらつきを足します。
  4. 回数に上限を付ける:何度でも送り直さず、数回でやめてエラーにします。

入口が何台もあるとき

ひとことで言うと

「どこで数えるか」で、上限の正確さが変わります。

台ごとに数えるゲートウェイ11秒1回ゲートウェイ21秒1回ゲートウェイ31秒1回app01速くて簡単。ただし振り分けられると合計で上限の3倍まで通ることがある共有の置き場で数えるゲートウェイ1ゲートウェイ2ゲートウェイ3共有の数app01:1秒1回全体で正しく数えられる。ただし毎回問い合わせるぶん、少し遅くなる入口が何台もあるときは「どこで数えるか」も決めておく
図14 入口が複数台あるときの数え方

ゲートウェイを何台も並べると、それぞれの台が自分で数えている場合、同じアプリのリクエストが振り分けられて、合計では上限より多く通ることがあります。Microsoft の設計ガイドは、台ごとの数は速いけれど少なく数えてしまい、共有の置き場(Redis など)で数えるとすべてを数えられるかわりに毎回の判断が少し遅くなる、と説明しています。上限を台数で割っておく方法もあります。どの方式でも、レート制限は「だいたい」で効くものだと考えておくと安全です。

試してみる(nginx で比べる)

ここからは、テスト用の環境で nginx 1.24.0 を動かし、同じ「1秒に1回」の数え方で、超えたときの扱いだけを変えて比べます。奥の API には、前回の記事と同じ Python の小さなサーバ(backend.py)を使います。

キーの値(demo-key-app01 など)は、この試験のために作った見本の値です。

設定を書いて動かす(図10の②〜④)

確かめること:同じ数え方のまま、場所(パス)ごとに超えたときの扱いを変えられること。

やっていること:お店の入口に、4種類の入店ルールを貼り出します。

ratelimit.conf(入口のルール)limit_req_zoneapp ごとに 1秒1回(rate=1r/s)location /strict/余裕なし。2回目からすぐ断るSTEP 2location /queue/3回まで並ばせて、順に通すSTEP 3location /nodelay/3回まで並ばせずに、すぐ通すSTEP 4location @too_many429 と Retry-After: 1 を返すSTEP 5location /dryrun/断らずに数えるだけSTEP 6log_format rl$limit_req_status で結果を記録STEP 3同じ数え方(1秒1回)で、超えたときの扱いだけを変えて比べる
図15 STEP 1 の設定の中身

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秒後にもう一度入ろうとします。

0 秒1 秒2 秒3 秒1回目200すぐ通る2回目(すぐ後)429すぐ断られる3回目(すぐ後)429すぐ断られる4回目(1秒後)200空きができて通る余裕なし(burst=0)だと、1秒に1回を少しでも超えたら断る
図16 STEP 2 の様子

/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組が同時に来ます。

0 秒1 秒2 秒3 秒1つめ200すぐ通る2つめ1秒待つ2003つめ2秒待つ2004つめ3秒待つ2005つめ429バケツがいっぱいで断られる5つ同時に送ると、1つは通り、3つは1秒おきに通り、1つは断られる
図17 STEP 3 の様子

/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組が同時に来ます。

0 秒1 秒2 秒3 秒1〜4つめ2004つともすぐ通る5つめ429空きがなく断られる6つめ(1秒後)2001つ分空いた7つめ(すぐ後)429また空きがないnodelay だと待たせずに通し、空きは1秒に1つずつ戻る
図18 STEP 4 の様子

/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秒後にどうぞ」の札を受け取り、その札どおりに出直すお客さんを用意します。

0 秒1 秒2 秒3 秒req1200req2429Retry-After: 1 → 1秒待つreq2(再送)200req3429Retry-After: 1 → 1秒待つreq3(再送)200言われた秒数だけ待てば、3回とも最後は 200 で終わる
図19 STEP 5 の様子

続けて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 では、断らずに通しながら、「本番なら断っていた」分を記録できること。

やっていること:入店制限を始める前に、入口で人数だけ数えて、「制限していたら何組を断っていたか」を記録します。

リクエスト返事記録($limit_req_status)1回目200PASSED上限の内側2回目(すぐ後)200REJECTED_DRY_RUN本番なら断っていた3回目(すぐ後)200REJECTED_DRY_RUN本番なら断っていたdry run は断らずに数えるだけ。本番の前に、止まる量を記録で確かめられる
図20 STEP 6 の様子

/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長い期間の総量回数制限の数え方:クラシックの階層はスライディングウィンドウ、v2 の階層はトークンバケットどちらも Retry-After ヘッダで、待つ秒数の目安を返す速さは rate-limit 系(429)、総量は quota 系(403)で決める
図21 Azure API Management の回数のポリシー
ポリシー何を数えるか超えたとき
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= の値)を確かめます。

困っていることよくある原因制限がかからないキーが空で数えていない・dry run のまま正しい利用者まで止まるIP で数えて NAT の裏がまとめて数えられる429 ではなく 503 が返るnginx の limit_req_status が初期値のままエラーではなく遅くなるburst だけで nodelay がなく、並ばされている429 がいつまでも続く待たずに送り直し、断られた分も数えられている台数を増やしたら緩くなった台ごとに数えているまず記録の limit= の値と、数えているキーを確かめる
図22 レート制限がうまくいかないとき

テスト用の環境で、キーを付けずに /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。奥には届かない「◯分後にどうぞ」の札
呼ぶ側言われた秒数だけ待つ。間を広げ、回数に上限を付ける時間をずらして出直す
nginxrate・burst・nodelay・dry run で調整速さ・行列の長さ・待たせ方・お試し期間
AzureAPIM の rate-limit 系(429)と quota 系(403)入店制限と回数券

参考にした資料

この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。

あとがき

nginx で burst と nodelay を付け替えて比べてみると、同じ「1秒に1回」でも、呼ぶ側から見た動きがまったく違うのがよく分かりました。burst だけだと 429 は減るものの、そのぶん3秒も待たされるリクエストが出ます。もう1つ印象に残ったのは、キーを付けないリクエストが1回も数えられずに素通りしたことです。設定を書いたら、キーがないとき・空のときにどう数えられるかまで、ログで確かめておきたいと思いました。次の記事では、入口でリクエストの中身を見て止める WAF を見ていきます。

コメント