ByteScope

429

HTTP 429 Too Many Requests の直し方

4xx クライアントエラーIETF 標準RFC 6585

レート制限が「頼みすぎだ」と判断しました。プロトコルの表明ではなくポリシーの表明なので、429 を返す 2 つのサービスがまったく別のものを数えていることもあります。

429 が出ている理由と、やるべきこと

同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。

curl で 429 を再現する

失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。

curl
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 が実際に意味していること

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 として姿を現しません。

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-AfterRFC 6585 は 429 においてこのヘッダーを MAY にしているので、無いことは合法ですしよくあります。そして RFC 9110 §10.2.3 は形式を 2 つ与えています——秒数の遅延か、HTTP-date か。数値形式しかパースしないクライアントは、日付のヘッダーを黙って 0 として扱い、即座に再試行します。1 回の 429 をより長いブロックに変える、いちばん速い方法です。
Cloudflare制限がエッジで適用されている場合、その応答を書いたのはオリジンではなく Cloudflare なので、そのリクエスト群についてアプリケーションのログには何も出ません。まさにそれが混乱の元です。`cf-ray` の値を持つブランド付きのページが目印で、その ray id がサポートの引ける手がかりです。訪問者に 429 が見えるかブロックが見えるかは、レート制限ルール自身の設定が決めます。
理由句
Too Many Requests
クラス
4xx クライアントエラー
定義
RFC 6585
標準
IETF 標準

このサイトのツール

どれもブラウザ内で完結します。アップロードは発生しません。

関連するステータスコード

429 を調べているとき、ついでに読むことになりがちなコードです。

よくある質問

このステータスコードを出したのがどのサーバーかを知るには?

ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、serverviacf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。

数字のあとの理由句(Not Found など)に意味はありますか?

ありません。分岐条件にしてはいけません。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 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。