Client Closed Request
ActiveHTTP 499 Client Closed Request is an nginx-specific non-standard status code logged when the client closes the connection before the server finishes sending the response. Not defined in any RFC. Indicates the response was never received — sent into the void. High 499 rates signal slow backend responses or aggressive client-side timeouts.
Description
The 499 Client Closed Request status code is an nginx-specific, non-standard HTTP status logged in the access log when a client terminates the connection before the server has finished delivering the response. This code never appears in actual HTTP responses — it exists purely as a logging artifact within nginx to distinguish premature client disconnections from other error conditions.
Unlike standard 4xx codes defined by IETF RFCs, 499 is an internal instrumentation code that nginx invented to give operators visibility into a specific failure mode: the server did its job (or was still working), but the client hung up before receiving the result. The server may have completed processing, partially sent the response, or still been waiting on an upstream — none of it matters because the recipient is gone.
The most common triggers are users navigating away during slow page loads, client-side HTTP timeouts firing before the server responds, mobile users losing connectivity mid-request, load balancer health check timeouts, and Kubernetes liveness/readiness probes with aggressive timeout configurations. In containerized environments, 499 floods are a telltale sign that probe timeouts are shorter than backend response times.
Other web servers handle this situation differently. Apache logs a broken pipe error or simply notes the incomplete transfer. Caddy, Traefik, and HAProxy have their own conventions. Only nginx promotes this to a distinct status code in the access log, making it uniquely searchable and alertable. This design decision makes nginx logs significantly more useful for diagnosing client-server timing mismatches.
From a debugging perspective, a sudden spike in 499s is almost always a backend performance issue in disguise. The clients aren't misbehaving — they're giving up because the server is too slow. The fix is rarely on the client side. Investigate upstream response times, database query performance, and connection pool exhaustion first.
Examples
192.168.1.42 - - [15/Jan/2024:10:28:03 +0000] "GET /api/reports/generate HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" "-"
request_time=12.004 upstream_response_time=- upstream_status=-# Client sets a 5s timeout, server takes 30s to respond
curl --max-time 5 https://api.example.com/api/reports/generate
# curl: (28) Operation timed out after 5001 milliseconds
# nginx logs: 499 — client closed connection before response was sent# Pod readiness probe config (timeoutSeconds < actual response time)
readinessProbe:
httpGet:
path: /healthz
port: 8080
timeoutSeconds: 3 # Probe gives up after 3s
periodSeconds: 10
# nginx access log shows 499 floods:
10.0.0.1 - - "GET /healthz HTTP/1.1" 499 0 request_time=3.001
10.0.0.1 - - "GET /healthz HTTP/1.1" 499 0 request_time=3.002
10.0.0.1 - - "GET /healthz HTTP/1.1" 499 0 request_time=3.001# nginx.conf — control behavior when client disconnects
http {
# If ON: nginx aborts the upstream request when client disconnects
# If OFF: nginx lets the upstream finish (useful for writes)
proxy_ignore_client_abort off; # default — logs 499
# Increase timeouts to reduce 499s caused by slow upstreams
proxy_read_timeout 300s;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
}GET /health HTTP/1.1
Host: backend-service.internal
Connection: close
User-Agent: ELB-HealthChecker/2.0
# Backend takes 6s to respond, ALB timeout is 5s
# ALB closes connection at 5s → nginx logs 499 # ALB marks target as unhealthy → cascading failure# PromQL: alert when 499 rate exceeds 5% of total requests
sum(rate(nginx_http_requests_total{status="499"}[5m]))
/
sum(rate(nginx_http_requests_total[5m]))
> 0.05
# Alert rule:
- alert: High499Rate
expr: nginx_499_ratio > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "High client disconnect rate ({{ $value | humanizePercentage }})"Edge Cases
- •499 is NEVER sent to the client as an HTTP response — it only exists in nginx access logs. The client never sees this code; it already disconnected.
- •proxy_ignore_client_abort controls nginx behavior AFTER the 499: when set to 'on', nginx lets the upstream request complete even though the client is gone (important for non-idempotent writes).
- •A 499 with upstream_response_time='-' means the upstream hadn't responded yet when the client disconnected. A 499 WITH an upstream_response_time means nginx received the upstream response but the client was already gone.
- •In Kubernetes, 499 floods from liveness probes can trigger a death spiral: probe times out → 499 → pod marked unhealthy → pod killed → remaining pods overloaded → more 499s.
- •Load balancers (ALB, NLB, HAProxy) sitting in front of nginx have their own idle timeouts. If the LB timeout is shorter than nginx's proxy_read_timeout, you get 499s that look like client errors but are actually infrastructure misconfiguration.
- •When debugging 499s, always check $request_time in the log — it tells you how long the client waited before giving up, which reveals whether the issue is client impatience or server slowness.
- •Some API gateways (Kong, APISIC) built on nginx inherit the 499 behavior. If you see 499s in Kong logs, the same debugging approach applies.
When You'll See This
- →User navigates away or closes browser tab while a slow page is loading
- →Client-side HTTP library timeout fires before server responds (axios timeout, fetch AbortController)
- →Mobile user enters a tunnel or elevator — connection drops mid-request
- →Kubernetes liveness/readiness probe timeout shorter than backend response time
- →Load balancer idle timeout expires before upstream responds — LB closes the connection
- →Browser cancels preflight or duplicate requests during rapid navigation (SPA route changes)
- →Automated test suite with aggressive timeouts hitting slow staging environment
- →Service mesh sidecar (Envoy) enforcing request timeout before nginx's upstream responds
Implementation References
| Language | Constant |
|---|---|
| nginx (C) | NGX_HTTP_CLIENT_CLOSED_REQUEST |
| Go (ingress-nginx) | 499 |
| Python (requests) | N/A — connection reset raises ConnectionError |
| Node.js (http) | N/A — socket 'close' event fires, no status code assigned |
| Kong (Lua/OpenResty) | ngx.HTTP_CLIENT_CLOSED_REQUEST (499) |
| OpenResty (Lua) | ngx.HTTP_CLOSE (444) / 499 logged implicitly |
| Prometheus (nginx-exporter) | nginx_http_requests_total{status="499"} |
| Envoy (C++) | DC (downstream connection termination) — mapped to 0, not 499 |
History
Introduced by Igor Sysoev in nginx's source code around 2004 as an internal logging status. The code appears in ngx_http_request.h as NGX_HTTP_CLIENT_CLOSED_REQUEST (value 499). It was never submitted to IANA or proposed as an RFC. Other servers (Apache, IIS, Caddy) handle the same situation differently — Apache logs a broken pipe or records the response code that would have been sent. Nginx's decision to create a distinct code made this failure mode first-class observable, which became critically important in the microservices era where client timeouts, service mesh proxies, and orchestrator health checks create complex timeout chains. The code gained renewed prominence with Kubernetes adoption (2015+) where ingress-nginx became the default ingress controller and operators needed to distinguish 'client gave up' from 'server failed'. Today it's one of the most-searched nginx-specific status codes.
Related Status Codes
Related Headers
FAQ
What causes HTTP 499 errors in nginx and how do I fix them?
499 means the client disconnected before receiving the response. The most common cause is slow backend responses — the client (browser, mobile app, load balancer, or K8s probe) has a timeout shorter than your server's response time. To fix: (1) identify which requests generate 499s by checking $request_uri in logs, (2) check $request_time to see how long the client waited, (3) optimize the slow endpoint (database queries, external API calls, computation), (4) if the endpoint is legitimately slow, increase client-side timeouts or add proxy_read_timeout in nginx config, (5) for K8s probes, increase timeoutSeconds. The fix is almost always 'make the server faster' not 'make the client wait longer'.
What is the difference between HTTP 499 and 408 Request Timeout?
408 is a standard HTTP status code (RFC 9110) sent BY the server TO the client when the server times out waiting for the client to finish sending the request. 499 is the opposite direction — the CLIENT gave up waiting for the SERVER. Additionally, 408 is an actual HTTP response that the client receives, while 499 is never sent as a response — it only exists in nginx access logs as an internal notation. Think of it as: 408 = server's patience ran out waiting for the client's request body; 499 = client's patience ran out waiting for the server's response.
Why do I see 499 errors with Kubernetes liveness and readiness probes?
Kubernetes probes have a timeoutSeconds setting (default: 1 second) that defines how long kubelet waits for a response before considering the probe failed. If your health endpoint takes longer than this timeout (due to database connection checks, dependency health aggregation, or garbage collection pauses), kubelet closes the connection and nginx logs a 499. This can cascade: repeated probe failures mark the pod as unhealthy, kubelet kills it, traffic shifts to remaining pods which become overloaded, causing more 499s. Fix by: (1) making health endpoints fast and dependency-free (liveness should just return 200), (2) increasing timeoutSeconds, (3) separating liveness from readiness — liveness checks if the process is alive, readiness checks if it can serve traffic.