499 Client Closed Request
Client error response defined by nginx.org.
Last updated July 27, 2026
Common causes at a glance
- An upstream slower than the client's configured timeout
- A user navigating away or hitting stop before the page finished
- A mobile client losing its connection mid-request
- An aggressive retry policy cancelling and reissuing requests
Reproduce it
curl --max-time 1 https://www.example.com/slow-reportWhat 499 Client Closed Request means
499 is a log entry, not a response. The client sent a request, nginx passed
it upstream, and while waiting for the application to answer the client closed the
socket. nginx notices, aborts the upstream request unless
proxy_ignore_client_abort is on, and writes 499 in the status field.
That makes 499 a measurement of your latency taken by your users. Nothing was wrong server-side, which is why it misleads on a dashboard: it counts as a 4xx, it looks like client error, and it means your endpoint was slower than someone’s patience. The server-side counterpart is 504, where the timeout was nginx’s.
There is no RFC
499 is unregistered and defined only by nginx’s implementation. You encounter
it through the $status variable in
access_log output,
which is the only place it exists. RFC 9110 §15 is beside the point for once: nothing is
sent, so no client has to decide how to treat an unrecognised code. The consequences are
about observability. Any rule shaped like “4xx rate above N” fires on 499, and any SLO
counting non-2xx as failures burns error budget on requests nobody was waiting for.
Exclude 499 from availability metrics and track it as a latency signal instead.
What actually causes it
Look at upstream response time on the 499 lines before blaming the client. If those requests sat near some round number — one second, five, thirty — that is a configured client timeout, and the question becomes whether the client is impatient or the endpoint is slow. A cluster of 499s on one route while the rest of the site is quiet points at that route. A flat background rate everywhere usually means mobile users on poor connections. Retry policies deserve suspicion: a short deadline plus automatic retries turns one slow request into a stream of 499s and duplicated upstream work, making the original slowness worse.
How to debug it
Log upstream timing so the code carries meaning. Add
$upstream_response_time and $request_time to your
log_format, then read the distribution rather than the count:
awk '$9 == 499 {print $NF, $7}' /var/log/nginx/access.log | sort -n | tail -20
Times clustered at an exact value are a client timeout. A long tail is genuine variance in your application.
Leave proxy_ignore_client_abort off so cancelled requests stop consuming
upstream capacity; turning it on means the backend keeps working on responses nobody will
read. Then fix the latency, or negotiate a longer deadline with the client team.
Working examples
curl
# Force the condition: a deadline shorter than the endpoint's latency.
curl --max-time 1 https://www.example.com/slow-report ; echo "exit=$?"
# exit=28 (Operation timed out) on the client; nginx logs 499.
# Measure the real latency so you know what deadline is reasonable.
curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
https://www.example.com/slow-report
# Useful log_format for diagnosing 499:
# log_format timed '$status $request_time $upstream_response_time $request_uri';
Python (requests)
import requests
url = "https://www.example.com/slow-report"
# (connect timeout, read timeout). Exceeding the read timeout closes the
# socket mid-response, which nginx records as 499.
try:
resp = requests.get(url, timeout=(3, 1))
print(resp.status_code)
except requests.exceptions.ReadTimeout:
print("client gave up; nginx logged 499")
# Measure before choosing a deadline.
print(requests.get(url, timeout=(3, 60)).elapsed.total_seconds())
Node (fetch)
// AbortSignal.timeout aborts the socket; the server sees a client-side close.
const url = "https://www.example.com/slow-report";
try {
const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
console.log(res.status);
} catch (err) {
if (err.name === "TimeoutError") console.log("aborted -> nginx logs 499");
else throw err;
}
// Retrying on abort multiplies 499s and upstream load. Measure first.
const start = performance.now();
await fetch(url);
console.log("actual latency ms:", Math.round(performance.now() - start));Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port