sfw/fix
499 medium

HTTP 499 Client Closed Request (Nginx)

Nginx logs 499 when the client drops the TCP connection before the upstream finishes responding.

What you see

GET /api/report HTTP/1.1" 499 0 "-" "Mozilla/5.0"
upstream_response_time: 30.001  request_time: 12.443

What’s actually happening

You see 499 only in nginx's access log, never in the browser — the client already left, so there's nothing to render. The response body size is usually 0. It clusters around slow endpoints: report generation, search, exports. The matching upstream (php-fpm, gunicorn, uWSGI, Node) often logs nothing, or logs a completed request that arrived too late.

Common causes

  • A slow backend: $request_time creeps toward the client's patience limit and the user closes the tab or the app cancels the fetch.
  • A reverse proxy or CDN in front of nginx (Cloudflare, an ALB) hits its own idle/read timeout and severs the connection first.
  • Load balancer health checks or uptime probes with a 1-2s timeout hitting a route that takes longer.
  • Mobile clients on flaky networks where the socket dies mid-request.
  • Client-side AbortController / axios timeout firing before the server responds.

How to fix it

  1. Confirm it's the backend that's slowAdd $upstream_response_time and $request_time to log_format. If upstream_response_time is high and close to your proxy_read_timeout, fix the backend (slow query, N+1, blocking I/O) — that's the real bug, 499 is just the symptom.
  2. Raise the impatient timeout, not nginx'sIf a fronting proxy/CDN is cutting the connection, raise its origin/read timeout (e.g. Cloudflare's 100s limit, an ALB idle timeout). For browser fetches, lengthen the client AbortController/axios timeout or return a faster partial response.
  3. Let critical work finish after disconnectFor endpoints that must complete even if the client leaves (payment capture, webhook processing), set proxy_ignore_client_abort on; in that location block so nginx keeps the upstream request alive. Use sparingly — it can pile up workers on truly slow routes.
  4. Make long jobs asyncMove multi-second work to a queue (Sidekiq, Celery, BullMQ) and return a 202 with a job ID immediately. Poll or push the result. This kills 499s at the source because the request returns fast.
  5. Filter out probe noiseIf health checks generate most 499s, point them at a cheap /healthz route and/or exclude that path from your error dashboards so real client aborts stay visible.

Stop it recurring

Keep p95 upstream_response_time well under every timeout in the chain (client, CDN, nginx) so no party gives up early.

Related errors