499エラーとは何か
Nginx 499はHTTP標準ではありません。サーバーがレスポンスを完了する前にクライアントが接続を閉じた際に、Nginxが記録するカスタムステータスコードです。/var/log/nginx/access.logに以下のような行が表示されます:
192.168.1.10 - - [27/Apr/2026:10:45:03 +0000] "GET /api/data HTTP/1.1" 499 0 "-" "Mozilla/5.0"
レスポンスボディサイズが0になっていることに注目してください。Nginxは何も送信できませんでした。クライアントが先に接続を切断したためです。
根本原因
バックエンドの処理が遅すぎたことが原因です。ブラウザ、モバイルアプリ、CLIツール、または上流のロードバランサーなどのクライアントが待ちきれずにTCP接続を閉じました。Nginxがまだ PHP-FPM、Node.js、uWSGI、またはデータベースクエリの応答を待っている間に、ソケットが切断されました。
よくある原因:
- インデックスの欠落により、本来50msで完了すべきデータベースクエリが8秒かかっている
- ブラウザの組み込みタイムアウト(通常30〜60秒)がバックエンドの応答より先に発火する
- 上流のロードバランサー(AWS ALB、HAProxy、Cloudflare)がより短いタイムアウトを持ち、先に接続を切断する
- ユーザーがリクエスト処理中にページを更新または離脱する
- モバイルクライアントが不安定な4Gネットワーク上で接続を切断する
1時間に数件の499は正常です。それはただユーザーがページを離脱しただけです。しかし数百・数千件となると、バックエンドに深刻な問題があります。
ステップ1 — 遅いエンドポイントを特定する
まだ設定を変更しないでください。最初に、どのURLが499を発生させているか、そしてそれらのリクエストが実際にどれくらいの時間かかっているかを調べます:
# URLパス別に499をカウント
grep ' 499 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
# 該当エンドポイントのリクエスト処理時間を確認(ログフォーマットに$request_timeが必要)
grep ' 499 ' /var/log/nginx/access.log | awk '{print $NF, $7}' | sort -rn | head -20
ログに$request_timeがない場合は、/etc/nginx/nginx.confに追加してください:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time';
その後リロード:nginx -s reload
ステップ2 — バックエンドを修正する(これが本質的な対処法)
499はあくまで症状です。タイムアウトを延ばすのは問題を隠しているだけです。まずバックエンドをプロファイリングして修正してください。
# PHP-FPMの場合:スローログを有効にして5秒以上かかるリクエストを検出
; /etc/php/8.1/fpm/pool.d/www.conf にて
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s
# Node.jsの場合:ブロッキング処理や未解決のPromiseを探す
# Python/Django/Flaskの場合:django-silkまたはpy-spyで遅いビューを特定する
# 特定のエンドポイントを直接計測する
curl -o /dev/null -s -w "Connect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://example.com/api/data
200msで完了すべき処理が12秒かかっている場合、ほとんどの場合は全件スキャンやN+1クエリが原因です。それを修正すれば、Nginxの設定を変えなくても499の80%は解消されます。
ステップ3 — Nginxプロキシのタイムアウトを調整する
CSVの一括エクスポートや数ヶ月分のデータを集計するレポートなど、本当に時間が必要なエンドポイントもあります。そういった場合は、グローバルではなく特定のlocationブロックのみタイムアウトを延ばしてください:
# /etc/nginx/conf.d/your-site.conf
location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 10s; # 上流への接続確立にかかる時間
proxy_send_timeout 60s; # 上流への書き込み間隔
proxy_read_timeout 60s; # 上流のレスポンスを待つ時間
proxy_http_version 1.1;
proxy_set_header Connection "";
}
遅いルートだけをオーバーライドしてください。全エンドポイントのタイムアウトを延ばさないようにしましょう:
location /api/export {
proxy_pass http://backend;
proxy_read_timeout 300s; # このエンドポイントのみ5分に設定
}
nginx -t && nginx -s reload
ステップ4 — ロードバランサーのタイムアウトを確認する
Nginxが別のレイヤーの背後にある場合、そのレイヤーがNginxのタイムアウトより先に接続を切断することがあります。
AWS ALBのデフォルトアイドルタイムアウトは60秒です。変更するには:EC2 → ロードバランサー → 対象のALBを選択 → 属性 → アイドルタイムアウト。
Cloudflareはすべてのプランでオリジンタイムアウトを100秒に制限しています。それ以上かかるエンドポイントには別のアプローチが必要です。チャンクレスポンス、WebSocket、またはCloudflare Workersへのオフロードを検討してください。
HAProxyの場合は、defaultsブロックのtimeout clientとtimeout serverを確認してください:
defaults
timeout connect 5s
timeout client 60s
timeout server 60s
ステップ5 — ユーザー操作による499に対処する
避けられない499もあります。ユーザーが悪いタイミングで更新を押すとNginxは499を記録しますが、上流のバックエンドはまだリクエストを処理し続けており、誰も待っていないワーカースレッドを消費し続けます。
Nginxに上流の処理を継続させることができます:
# /etc/nginx/nginx.conf(httpブロック)
proxy_ignore_client_abort on;
この設定は慎重に使用してください。クライアントが切断した後も上流の接続を維持し、サーバーリソースを占有し続けます。5秒以内の短いリクエストには問題ありませんが、長時間かかるエクスポート処理では、ゾンビ接続が積み重なる恐れがあります。
ステップ6 — 修正を確認する
# リアルタイムで499の減少を監視する
tail -f /var/log/nginx/access.log | grep ' 499 '
# 5分間、1分ごとに499をカウントする
for i in $(seq 5); do
echo -n "$(date +%H:%M): "
grep ' 499 ' /var/log/nginx/access.log | grep "$(date +%d/%b/%Y:%H:%M)" | wc -l
sleep 60
done
# 遅いエンドポイントが改善されたか確認する(ログのurt=の値を確認)
grep '/api/data' /var/log/nginx/access.log | tail -20
予防策
- **グローバルではなくルート単位でタイムアウトを設定する。**高速なエンドポイントは素早く失敗させるべきです。設計上遅いルートにのみ長いタイムアウトを設定してください。そうしないと、暴走したクエリがどこでも300秒の猶予を得ることになります。
- **499の件数ではなく発生率でアラートを設定する。**499が全リクエストの1〜2%を超えたとき、それは単なるユーザーの「戻る」ボタン操作ではなく、真に問題が発生しているサインです。PagerDuty、Grafana、または利用中のツールに組み込んでください。
- **遅い処理をHTTPスレッドから切り離す。**リクエストが常に5〜10秒以上かかる場合は、すぐにジョブIDを返してクライアントにポーリングさせてください。負荷がかかっている状態でHTTP接続を30秒間保持し続ければ、必ず499が発生します。
- **データベース接続をプールする。**PostgreSQLならPgBouncer、またはORMの接続プールを活用してください。利用可能なDB接続を待つリクエストは、APIバックエンドにおける499の最も一般的な原因です。

