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
- 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.
- 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.
- 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.
- 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.
- 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