ByteScope

504

HTTP 504 Gateway Timeout の直し方

5xx サーバーエラーIETF 標準RFC 9110

プロキシが上流を待ち、しびれを切らして代わりに答えました。だから犯人を特定する数字はステータスコードではなく経過時間のほうです。それは必ず誰かが設定したタイムアウトです。

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

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

curl で 504 を再現する

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

curl
curl -sS -o /dev/null -D - --max-time 180 -w '\n%{http_code} connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' https://example.com/slow

`--max-time 180` は curl の全体の上限で、サーバー側のタイマーよりゆとりをもって長くしておく必要があります。そうしないと curl が先に中断し、自分がせっかちだったこと以外は何も分かりません。診断になるのは 3 つの時刻です。`%{time_connect}` は TCP 接続が完了した時点、`%{time_starttransfer}` は最初のバイトが届いた時点、`%{time_total}` は終わりです。504 は接続が速く、合計がきりのいい数字に着地します。その数字を文書化された既定値と突き合わせれば層が分かります。60 秒前後なら nginx の `proxy_read_timeout` か Application Load Balancer のアイドルタイムアウト——どちらも既定 60 秒——で、125 秒前後なら Cloudflare の読み取りタイムアウトです。ただしそれは 504 ではなく 524 として報告されるので、125 秒の 504 は Cloudflare のものではありません。どの既定値より明らかに短い合計は、誰かが意図的に設定したタイマーで、製品ではなく設定ファイルに絞り込めます。次に層を分解します。`--resolve example.com:443:203.0.113.10` は元のホスト名と SNI を送りつつオリジンのアドレスへ直接つなぐので、エッジが 60 秒で諦めたのにオリジンが 90 秒で答えるなら、作業は本当に遅く、タイマーは伝令にすぎません。コマンドの実行中はオリジン自身のアクセスログを見ていてください。タイムアウトより長い所要時間でそこに現れるリクエストは、プロキシが待つのをやめたあとに作業が完了した証拠です。ここで書き込みを再試行するのが危険な理由は、まさにこれです。

504 が実際に意味していること

RFC 9110 §15.6.5 は 504 を、ゲートウェイまたはプロキシとして動作しているサーバーが、リクエストを完了するためにアクセスする必要のあった上流サーバーから、時間内に (timely) 応答を受け取れなかったときに返すステータスと定義しています。不正な応答のために 502 を取ってある §15.6.3 と並べて読むと、この「時間内に」という一語が区別のすべてで、しかもストップウォッチが付いてきます。502 は何かが拒否したか壊れたのでミリ秒で返り、504 はタイマーが切れたのできりのいい数字で返ります。そのきりのいい数字は文書化されていて、層を特定できます。nginx は `proxy_read_timeout`・`proxy_connect_timeout`・`proxy_send_timeout` をそれぞれ既定 60 秒とし、接続タイムアウトは通常 75 秒を超えられない——OS 自身の接続試行がそこで終わるため——と注記しています。AWS は Application Load Balancer の接続アイドルタイムアウトを既定 60 秒、1 から 4000 まで設定可能と説明し、長いアップロードが切られないようアイドル期間が経過する前に少なくとも 1 バイトを送ることを勧めています。Cloudflare 自身の読み取りタイムアウトは 125 秒で、しかも 504 ではなく 524 を生みます。つまり 1 分あたりの 504 は nginx か ALB の既定タイマー、それ以外のきりのいい数字の 504 は誰かが意図的に設定したタイマーです。次の一手を変える性質が 2 つあります。504 は RFC 9110 §15.1 がヒューリスティックにキャッシュ可能と定めるステータスコードには入っていないので、何も指示されていない中間装置が保存することはなく、リクエストごとに作り直されます。そして 502 と違い、504 は作業が実行されたかどうかについて何の証拠も運びません。プロキシが諦めた時点でも上流は接続を握っていたので、その 1 秒後にリクエストを完了させ、頼まれたことを全部書き終えている可能性が十分にあります。だから非冪等なリクエストの無思慮な再試行が、ここでいちばん高くつく間違いです。nginx も同じ用心を実装しています。`proxy_next_upstream` の既定は `error timeout` なのでタイムアウトはグループ内の次のサーバーへ回りますが、非冪等なメソッドのリクエストは、いったん送られたあとは `non_idempotent` を明示しない限り回されません。このパラメータは nginx 1.9.13 からあります。

504 と間違えられやすいコード

紛らわしいコード見分け方
502どちらも中間装置が上流について報告したものですが、RFC 9110 は正確に線を引いています。§15.6.3 は不正な応答、§15.6.5 は時間内に応答が無かった場合です。文言よりも時計のほうが速く決着をつけます。502 は接続が拒否されたか切れたのでミリ秒で返り、504 はタイマーどおりに返ります。ラベルより大事なのは帰結です。502 のあとならリクエストはほぼ確実に何もしていませんが、504 のあとは上流が完了させているかもしれません。
524524 は IETF のものではなく Cloudflare 独自のコードで、まさにこの状況を、まさに 1 か所について表します。Cloudflare がオリジンに接続したのに、その 125 秒の読み取りタイムアウト内に HTTP 応答が届かなかった、という状態です。だから Cloudflare 越しに届いた 504 は別の何か——自前のプロキシか、ゲートウェイとして動いているオリジン——が生成し、中継されたものです。タイムアウトを延ばしたい場合、Cloudflare のそれを変更できるのは Enterprise ゾーンだけで、上限は 6,000 秒です。
503503 は前もって下した拒否、504 は切れてしまった待ち時間です。RFC 9110 §15.6.4 が 503 に `Retry-After` を載せられるようにしているのは、サーバーがいつ戻れるかを大まかに知っているからです。504 に相当するものはありません。プロキシには上流が何をしているのか見当もつかないからです。負荷の切り捨てが 504 を生んでいるなら、それは負荷を切り捨てていません。タイマーに殺されるまでリクエストを並べているだけで、上流はどのみち作業のコストを払っています。
nginxエラーログがタイマーを名指しするので、3 つのメッセージは区別して覚える価値があります。`while connecting to upstream` は `proxy_connect_timeout` で、ネットワークかリスナーの不在を指します。`while reading response header from upstream` は `proxy_read_timeout` で、遅いアプリケーションを指します。`while sending request to upstream` は `proxy_send_timeout` で、たいていは止まった読み手に対する大きなリクエストボディです。3 つとも既定 60 秒で、接続タイムアウトは通常 75 秒を超えられません。
ELB 504AWS は Application Load Balancer の接続アイドルタイムアウトを既定 60 秒、1 から 4000 まで調整可能と説明し、ターゲットがその時間内に応答しない場合の 504 についても書いています。長い処理に対する AWS 自身の助言は、各アイドル期間が経過する前に少なくとも 1 バイトを送ること。ストリーミングの応答が生き残り、無言の応答が死ぬ理由がこれです。ターゲット側の keep-alive も確認してください。AWS は、ロードバランサーのアイドルタイムアウトより短くすべきではないと注記しています。
理由句
Gateway Timeout
クラス
5xx サーバーエラー
定義
RFC 9110
標準
IETF 標準

このサイトのツール

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

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

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

よくある質問

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

ページではなく応答ヘッダーを読んでください。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 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。

504 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。