Skip to content
Control Plane Labs

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

What 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