429
レート制限が「頼みすぎだ」と判断しました。プロトコルの表明ではなくポリシーの表明なので、429 を返す 2 つのサービスがまったく別のものを数えていることもあります。
同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。
これが出ている理由
たいていは自分のコード、それも書いたつもりのないループです。繰り返し現れる形はいくつかあります。依存が毎レンダーで変わる `useEffect`——1 回のリクエストがフレームごとのリクエストになります。バックオフの無い再試行——1 回の失敗がバーストに変わります。複数のコンポーネントがマウント時にそれぞれ同じエンドポイントを叩く。そして 401 のたびに走るトークンリフレッシュ——期限切れのセッションが、いちばん制限されやすいエンドポイントへの連射を生みます。まったくあなたのせいではない場合もあります。制限が IP アドレスに紐づいていると、オフィスやキャンパスや VPN の出口は数百人を 1 人の非常に忙しいクライアントに見せるので、ごく普通の使い方が他人の予算に着地します。
やること
応答の `retry-after` を読み、そのとおりに従ってください。RFC 9110 §10.2.3 は秒数と HTTP-date の両方を許しているので、数値形式だけを前提にせず両方をパースします。無い場合は、ジッター付きの指数バックオフと試行回数の明確な上限で下がってください。多数のクライアントが同期して再試行すると、制限を招いたバーストをそのまま作り直すことになります。飛行中のリクエストはキーで重複排除して、5 つのコンポーネントが同じものを求めても呼び出しは 1 回になるようにし、結果はページの寿命のあいだキャッシュし、レンダーの中では絶対に再試行しないこと。ループかどうかを確かめるには、ネットワークパネルを時刻でソートして、UI では説明のつかない間隔で同じ URL が繰り返されていないかを見ます。そして認証エンドポイントに出る 429 は、忙しいサーバーの証拠ではなくリフレッシュループの証拠として扱ってください。
これが出ている理由
まず、自分が制限装置のどちら側にいるのかを決めてください。この 2 つの状況には共通点がありません。クライアント側なら、罠は共有された出口です。1 つの NAT アドレスの後ろから他人の API を呼ぶサーバー群は、IP 単位の制限にとって 1 クライアントなので、ユーザーあたりでは些細に見えるトラフィックが合計で天井を叩きますし、スケールアウトは事態を改善せず悪化させます。制限装置側なら、その応答は自分のもので、その体裁も自分が決めています。Cloudflare のレート制限ルールはアクションを選べるので、同じ制限装置が 429 として出ることもブロックページとして出ることもありますし、GitHub は制限超過が 403 でも 429 でも返りうると明記しています。nginx の既定はもっと意外です。`limit_req` は `limit_req_status` を返し、変更しない限りそれは 503 で、エラーログには `limiting requests, excess: ... by zone ...` と書かれます。つまりこのログ行を伴う 503 は、別の番号を着たレート制限です。
やること
クライアント側なら、プロセスごとではなく外向き呼び出しの手前に共有の制限装置を 1 つ置き、提供側がキーにしているものと同じキーで数え、提供側の残数とリセットのヘッダーを読んでください。使い切って発見するのではなく、予算を知ることができます。制限装置側なら、どの数字を上げるより先に設定のゾーンとキーを読んでください。プロキシの後ろで `$binary_remote_addr` をキーにした制限装置はプロキシのアドレスしか見ないので、本物のクライアントアドレスを先に復元しない限り、インターネット全体を 1 クライアントとして絞ります。`limit_req_status 429` を設定して、クライアントがレート制限と障害を区別できるようにしてください。そして RFC が示唆しているだけであっても `Retry-After` を送ること。待つクライアントと叩き続けるクライアントの差はここにあります。あとはエラーログでゾーン名を確認すれば、どのルールが発火したかが分かります。
これが出ている理由
サイトは、あなたから、あなたのネットワークから、あるいはあなたの代わりに動いているアプリから、最近リクエストが多すぎたと言っていて、少し休んでほしいと伝えています。たいていは意図的にやったことではありません。裏で更新し続ける拡張機能、同じサイトを開いたままの複数のタブ、自分でリロードするページ、あるいはオフィス・キャンパス・VPN の共有アドレスで、その後ろにいる全員が 1 人の訪問者として数えられている、といったところです。これは設計上一時的なもので、サイトが自分を負荷から守っているだけです。端末の不調でも、締め出しでもありません。
やること
待ってください。そしてリロードを我慢してください。ほとんどの制限は数秒から数分で解けますが、リロードを繰り返すとカウンターが満杯のまま維持されるので、更新を連打すると短くなるどころか長引きます。同じサイトの重複したタブを閉じ、そのサイトをポーリングする拡張機能やスクリプトを止め、VPN や共有ネットワークを使っているなら外して試してください。そのアドレスにいる全員がまとめて数えられているかもしれません。ページに待ち時間が書かれていれば、その数字がサイト自身の答えで、手に入る中でいちばん確かな最短時間です。いつまでも解けないなら、それは報告する価値があります。
失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。
curl -sS -o /dev/null -w '%{http_code} %header{retry-after}\n' 'https://example.com/api/health?n=[1-50]'角括弧は curl の URL グロブなので、これは 1 つのリソースへの 50 回の逐次リクエストで、クエリ文字列を変えることでキャッシュを避けています。各回が 1 行を出力します。`%header{retry-after}` は curl 7.84 以降が必要で、そのヘッダーを出力し、サーバーが送っていなければ何も出しません。注目すべき行は切り替わりです。200 が 429 に変わったリクエスト番号が、制限装置が許すバーストの大きさです。同じコマンドをすぐもう一度実行すれば、窓が固定式かスライド式かも分かります。即座に 2 回目の 429 なら窓はまだ回っておらず、新しい枠が出るなら回っています。`retry-after` の列が空なのも発見です。RFC 6585 がこのヘッダーを MAY にしている以上、無いということは、どのクライアントも待ち時間を推測するしかないということです。その列に 503 が現れないかも見てください。nginx の `limit_req` は既定で 503 を返すので、一定のリクエスト番号で 503 に切り替わる実行結果も、やはりレート制限です。バーストの大きさではなく持続可能なレートを知りたいなら `--rate 10/s`(これも curl 7.84 以降)を足して間隔を刻み、429 が出なくなるまで数字を下げていきます。
429 は RFC 9110 には載っていません。出どころは RFC 6585 §4 で、一定時間内にユーザーがリクエストを送りすぎた状態と定義し、応答の表現でその状況を説明すべき (SHOULD)、どれだけ待てばいいかを示す `Retry-After` ヘッダーを含めてもよい (MAY) としています。どちらも意図的に緩く、続く文はさらに緩いです。RFC 6585 は、オリジンサーバーがユーザーをどう識別するかも、リクエストをどう数えるかも定義しない、とはっきり書いています。この一文が、このコードのふるまいがサービスごとにこれほど違う理由です。あるサービスはトークン単位、別のサービスは IP アドレス単位、また別のサービスはエンドポイント単位やアカウント単位で数えていて、どれも何にも違反していません。同時にこれは、目の前の数字が「応答からは読めないポリシー」についての表明だということでもあるので、1 回の 429 から窓の長さを推測するのは当て推量です。`Retry-After` 自体は RFC 9110 §10.2.3 の定義で、クライアントが扱わなければならない構文が 2 つあります——秒数の遅延か、HTTP-date か。しかも RFC 6585 では MUST ではなく MAY なので、どちらも送らない制限装置はいくらでもあります。残り回数のヘッダーにも、全員が合意した標準はありません。事実上の標準は `X-RateLimit-Limit`・`X-RateLimit-Remaining`・`X-RateLimit-Reset` で、たとえば GitHub は `x-ratelimit-reset` を UTC の epoch 秒として文書化しているため、変換しないと意味を持ちません。時間を節約する事実があと 2 つ。429 は RFC 9110 §15.1 のヒューリスティックにキャッシュ可能なコードには入っていないので、キャッシュが独断で保持することはありません。そして、制限装置が拒否を伝える方法はこのコードだけではありません。nginx の `limit_req` モジュールは `limit_req_status` で答え、その既定値は 503 です。だから現実のレート制限のかなりの部分は、そもそも 429 として姿を現しません。
| 紛らわしいコード | 見分け方 |
|---|---|
| 503 | この組み合わせでいちばん高くつく取り違えは、nginx 自身のレート制限が 429 ではなく 503 で答えることです。`limit_req_status` の既定値は 503 なので、負荷まわりの 503 でエラーログに `limiting requests, excess: ... by zone ...` があれば、それは障害ではなく制限です。どちらのコードも `Retry-After` を運べるので、ヘッダーでは分かれません。分けるのはログの行と、失敗がリクエスト量に追随しているかどうかです。 |
| 403 | 制限装置が 429 以外で拒否してもかまいません。GitHub はレート制限の超過が 403 でも 429 でも返りうると明記していますし、Cloudflare のレート制限ルールはアクションをブロックに設定できます。だから負荷が高いときだけ現れて待てば消える 403 は、権限の問題ではなく制限です。ロールをいくら棚卸ししても何も見つかりません。 |
| 401 | この 2 つは互いを生みます。401 のたびにトークンをリフレッシュするクライアントは、1 つの期限切れセッションを認証エンドポイントへのバーストに変えます。そこはいちばんレート制限されやすいエンドポイントで、結果の 429 は無関係な障害のように見えます。429 がトークンのエンドポイントに集中しているなら、バグはサーバーの制限ではなくクライアントのリフレッシュループです。 |
Retry-After | RFC 6585 は 429 においてこのヘッダーを MAY にしているので、無いことは合法ですしよくあります。そして RFC 9110 §10.2.3 は形式を 2 つ与えています——秒数の遅延か、HTTP-date か。数値形式しかパースしないクライアントは、日付のヘッダーを黙って 0 として扱い、即座に再試行します。1 回の 429 をより長いブロックに変える、いちばん速い方法です。 |
Cloudflare | 制限がエッジで適用されている場合、その応答を書いたのはオリジンではなく Cloudflare なので、そのリクエスト群についてアプリケーションのログには何も出ません。まさにそれが混乱の元です。`cf-ray` の値を持つブランド付きのページが目印で、その ray id がサポートの引ける手がかりです。訪問者に 429 が見えるかブロックが見えるかは、レート制限ルール自身の設定が決めます。 |
どれもブラウザ内で完結します。アップロードは発生しません。
429 を調べているとき、ついでに読むことになりがちなコードです。
ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、server・via・cf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。
ありません。分岐条件にしてはいけません。RFC 9112 はクライアントに理由句を無視するよう求めていますし、サーバーは自由に変えられますし、HTTP/2 と HTTP/3 にはそもそも理由句が存在しません。HTTP/1.1 では 404 Not Found として届くものが、HTTP/2 では素の 404 として届きます。このサイトが登録済みの理由句を載せているのは、それが検索される語であり HTTP/1.1 のログに出る文字列だからで、何かのソフトウェアがそれに依存しているからではありません。
5xx と 429 は再試行してよく、4xx はしないでください。次に投げても中身は同じだからです。応答に Retry-After が付いていればそれに従ってください。RFC 9110 がまさにこのために定義していて、値は秒数でも日時でも構いません。付いていなければ、ジッター付きの指数バックオフと明確な上限を使い、しかも冪等なメソッドに限ってください。再試行された POST はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。
変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。
429 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。