# 499 Client Closed Request (nginx)

> nginx 499 means the client hung up before the response was sent. Why it spikes with slow upstreams, how proxy_ignore_client_abort works, and how to debug it.

Source: https://howhttpworks.com/status-codes/499
Last reviewed: 2026-10-05

> **TL;DR:** 499 is nginx's log-only code for "the client closed the connection before I could answer." Nothing is sent over the wire; it shows up in `access.log` and is usually caused by clients timing out on a slow upstream, users navigating away, or a load balancer in front with a shorter timeout than nginx.

## What it means

499 is not in any RFC. nginx defines it internally as `NGX_HTTP_CLIENT_CLOSED_REQUEST` so the access log has something to record when a request ends because the client's TCP connection (or, on HTTP/2 and HTTP/3, the stream) went away mid-request. A browser, curl or SDK can never receive a 499 from nginx, because by definition the connection is gone. If you see 499 in a client, a different proxy or gateway produced it.

A typical access log line, using the default `combined` format:

```text
203.0.113.7 - - [04/Oct/2026:10:15:32 +0000] "POST /api/reports/export HTTP/1.1" 499 0 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)"
```

Body bytes sent is `0`, which is the giveaway. The line only becomes useful once you add timing to the log format:

```nginx
log_format timed '$remote_addr [$time_local] "$request" $status '
                 '$body_bytes_sent rt=$request_time urt=$upstream_response_time '
                 'uct=$upstream_connect_time ua="$http_user_agent"';
access_log /var/log/nginx/access.log timed;
```

```text
203.0.113.7 [04/Oct/2026:10:15:32 +0000] "POST /api/reports/export HTTP/1.1" 499 0 rt=30.001 urt=- uct=0.001 ua="okhttp/4.12.0"
```

`rt=30.001` with `urt=-` (the upstream had not finished) means the client waited exactly 30 seconds and left. A round number like that is almost always a client-side timeout constant. Find who owns it.

nginx also notes the event in `error.log`, but only at `info` level, so you will not see it unless you run `error_log ... info;`:

```text
2026/10/04 10:15:32 [info] 1234#1234: *5678 client prematurely closed connection, client: 203.0.113.7, server: api.example.com, request: "POST /api/reports/export HTTP/1.1", upstream: "http://10.0.2.15:8080/api/reports/export"
```

## Who closed the connection?

The closer is the party directly in front of nginx, which is not always the end user.

| Direction | What to check |
|---|---|
| Browser or mobile app | Round timeout values in `$request_time` (10, 15, 30, 60 seconds) match an HTTP client default. The user agent shows the SDK (`okhttp`, `axios`, `python-requests`). Users navigating away or hitting reload also produce 499s, with varied durations. |
| Cloud load balancer in front of nginx | If the LB idle timeout (AWS ALB default: 60 s) is shorter than your nginx and app timeouts, the LB gives up on the target and closes the connection, so nginx logs 499 while the ALB returns [504](https://howhttpworks.com/status-codes/504) to the client. An ALB [460](https://howhttpworks.com/status-codes/460) is different: the ALB's own client hung up first. If every 499 comes from a private LB address, the closer is the LB. |
| Health checkers and monitors | Probes with a 1-5 second timeout against a slow endpoint produce steady 499s. Filter on user agent (`ELB-HealthChecker`, `kube-probe`, `Pingdom`). |
| HTTP/2 clients | A stream cancel (`RST_STREAM` with `CANCEL`), such as from `AbortController.abort()` or a navigated-away page, is logged as 499 the same way. |
| CDN in front | CDNs have their own origin timeouts (see [524](https://howhttpworks.com/status-codes/524)); when they abandon the origin connection, nginx sees a closed client. |

Mobile networks inflate the numbers: a client on a flaky connection that backgrounds the app sends a FIN or simply vanishes, and nginx records 499 once it notices. A small steady baseline of 499s on a consumer-facing API is normal. A sudden jump, or a cluster on a single endpoint, is the signal.

## Fix it

1. **Confirm the upstream is slow.** Group 499s by URI and compare `$upstream_response_time` for 200s on the same route. If p95 is near the clients' timeout, the fix is making the endpoint faster (indexes, caching, or moving the work to a queue and returning [202](https://howhttpworks.com/status-codes/202)).
2. **Align the timeout chain.** Each hop's timeout should be longer than the one behind it, so the layer closest to the work gives up first with a meaningful error: app < nginx `proxy_read_timeout` (default 60 s) < load balancer idle timeout < client timeout. When the client is shortest you get 499 and no useful error anywhere.
3. **Stop abandoned work from piling up.** Make sure the app notices a closed connection: in Node, listen for `req.on('close')` and cancel the database query; in Go, honour `r.Context().Done()`. Under gunicorn or PHP-FPM the worker generally keeps going, so rely on query timeouts.
4. **Make retries safe.** A client that times out and retries a `POST` while the first one is still running creates duplicates. Use an idempotency key.
5. **Only then consider `proxy_ignore_client_abort`.**

```nginx
location /webhooks/ {
    proxy_pass http://app_backend;

    # Keep the upstream request running even if the sender disconnects.
    proxy_ignore_client_abort on;
}
```

The default is `off`. When it is `on`, nginx does not close the upstream connection when the client leaves, so the backend completes the request. The directive exists for work that must finish regardless, such as webhook receivers whose sender has a short timeout. On slow endpoints it is counterproductive: abandoned requests keep consuming workers, clients retry, and load amplifies. The equivalents for other backends are `fastcgi_ignore_client_abort`, `uwsgi_ignore_client_abort` and `scgi_ignore_client_abort`.

## Kubernetes and ingress-nginx

ingress-nginx logs 499 in its access log exactly as stock nginx does, in a format that includes upstream timings and a request ID. The usual cause is a mismatch between the cloud load balancer in front of the ingress and the ingress timeouts. Timeouts are set per Ingress through annotations:

```yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
```

If a cloud LB with a 60 s idle timeout sits in front, raising the annotation to 120 changes nothing: the LB closes first and the ingress logs 499.

## Reproduce it

```bash
# Client gives up after 2 seconds against a slow endpoint
curl -m 2 -i https://api.example.com/slow
# curl: (28) Operation timed out after 2001 milliseconds with 0 bytes received

# nginx access log now has: "GET /slow HTTP/1.1" 499 0 rt=2.001
```

## Related

- [504 Gateway Timeout](https://howhttpworks.com/status-codes/504): nginx gave up on the upstream, client still connected.
- [502 Bad Gateway](https://howhttpworks.com/status-codes/502): the upstream answered badly or dropped the connection.
- [408 Request Timeout](https://howhttpworks.com/status-codes/408): the server gave up waiting on a slow client.
- [524 A Timeout Occurred](https://howhttpworks.com/status-codes/524): Cloudflare's equivalent when the origin is slow.
- [444 Connection Closed Without Response](https://howhttpworks.com/status-codes/444): nginx closes the connection on purpose.
