Skip to content
RU
← All articles

Nginx 499 Client Closed Request: What It Means and What to Fix

In short. 499 is not an HTTP status code — it is an nginx log entry recording that the client hung up before a response was ready. Nobody ever receives a 499. It matters because it is a leading indicator: clients give up before servers do, so a 499 spike usually arrives minutes before the 502s and 504s that get everyone's attention.

Nobody sees a 499, which is why it is misread

The code does not exist in RFC 9110. Nginx invented it to have something to write in the access log when a connection closes while a request is still in flight. Apache, IIS and Caddy have no equivalent, so the same event in those servers is simply absent from the log.

Two consequences follow. First, a user reporting an error is never reporting a 499 — they saw a spinner, a browser timeout, or they navigated away. Second, a 499 in your log is a statement about the client, and the useful question is why the client stopped waiting.

Because the code appears only on the server side and is never returned, teams routinely treat it as harmless log noise. That is right about half the time. The other half it is the earliest visible symptom of an upstream that is about to fail outright.

Diagram of a request in flight where the client connection is cut before the server produces a response, with the log entry written after the fact
The connection closes while work is still in progress. The log entry records an absence, not a delivered response.

The one measurement that splits the causes

All 499s look identical in a default log line. Add two variables and they split cleanly into two very different problems.

# nginx.conf — a log format that makes 499 diagnosable
log_format timed '$remote_addr $status rt=$request_time urt=$upstream_response_time '
                 'ua="$http_user_agent" "$request"';

access_log /var/log/nginx/access.log timed;

$request_time is how long the request lived before the connection died. That single number separates the two families:

$request_time on the 499What happenedOwner
Close to a round number — 30 s, 60 s, 100 sSomething timed out on a fixed limitTimeout configuration
Long but scatteredHumans gave up waiting at their own paceApplication performance
Very short — well under a secondNavigation away, cancelled fetch, scannerUsually nothing to fix
Short and from one user agentA health check or bot with a tight timeoutThe checker's configuration
# Where do your 499s actually cluster?
awk '$2 == 499 { print $3 }' /var/log/nginx/access.log \
  | sed 's/rt=//' | sort -n | uniq -c | tail -20

# Which endpoints produce them?
awk '$2 == 499' /var/log/nginx/access.log | grep -oE '"[A-Z]+ [^ ]+' | sort | uniq -c | sort -rn | head -10

If the values pile up on one number, you are looking at a timeout, and the number tells you which layer owns it. If they are spread out, you are looking at people abandoning a slow page.

Cause 1: the timeout ladder is in the wrong order

This is the single most common real cause, and it is a configuration mistake rather than a fault. Every layer in the path has its own timeout, and their relative order decides whether a failure produces a diagnosable error or a silent abandonment.

The rule is that the innermost timeout should be the shortest and each outer layer should allow slightly more. Then whichever component gives up first is the one closest to the work, and it returns a real status code that everything outside can log, alert on and show.

LayerTypical defaultShould be
Application workeroften 30 sShortest — fails first, returns an error
nginx proxy_read_timeout60 sSlightly longer than the application
Load balancer / CDN60–100 sLonger than nginx
Client / browservaries, often longerLongest

Reverse any pair and you manufacture 499s. If the load balancer gives up at 60 seconds while nginx is willing to wait 300, the balancer severs the connection, nginx logs 499, and you never learn what the upstream would eventually have said. The error that would have told you the cause was thrown away by the layer that quit early.

# What is nginx actually configured to wait?
nginx -T 2>/dev/null | grep -E 'proxy_(read|send|connect)_timeout|fastcgi_read_timeout'

# And the application behind it — gunicorn as an example
grep -E '^\s*timeout' /etc/gunicorn.conf.py 2>/dev/null

Raising the outer timeout to match the inner one removes the 499 and replaces it with a visitor who waits five minutes. Fixing the ordering is a different action from raising limits: the goal is that the component doing the work is the one that decides to stop.

Nested timeout bars showing an inner layer with the shortest limit and each outer layer allowing progressively more time
Correctly ordered, the innermost layer fails first and returns a real error. Reversed, an outer layer abandons and the diagnosis is lost.

Cause 2: the upstream really is slow

When 499 times are long and scattered rather than clustered, humans are abandoning a page that takes too long. The 499 is then a symptom and the upstream is the cause.

Confirm by comparing the two timers on requests that did complete. If $upstream_response_time is close to $request_time, nginx spent its time waiting for the application rather than on the network.

# Slowest completed requests, by time spent in the upstream
awk '$2 != 499 { gsub(/urt=/,"",$4); print $4, $0 }' /var/log/nginx/access.log \
  | sort -rn | head -10 | cut -c1-140

# Do 499s concentrate on the same endpoints as the slow completions?

They usually do, and that overlap is the confirmation. Fixing the endpoint removes both the 499s and the 504s waiting behind them. The neighbouring guides on where server time goes and gateway timeouts cover that work.

Cause 3: normal client behaviour

Some 499s are simply people. A visitor clicks a link before the page finishes, closes a tab, or a single-page application cancels an in-flight request because the user typed another character into a search box. Aborting a superseded request is correct client behaviour, and the resulting log entry is not a defect.

The signature is a very short request time, often on the same endpoint, frequently a search or autocomplete path. Before spending effort, check what proportion of your 499s look like this:

# Share of 499s that lasted under one second
total=$(awk '$2 == 499' /var/log/nginx/access.log | wc -l)
quick=$(awk '$2 == 499 { gsub(/rt=/,"",$3); if ($3 < 1) c++ } END { print c+0 }' /var/log/nginx/access.log)
echo "$quick of $total 499s completed in under a second"

A high share here means the number is inflated by ordinary interaction, and the alert threshold should account for it rather than paging someone every afternoon.

Cause 4: health checks and bots with tight timeouts

Monitoring agents, uptime checkers and crawlers set their own deadlines, sometimes very short ones. A checker configured with a two-second timeout against an endpoint that normally takes three will generate a steady drip of 499s and, more importantly, will report your site as down while it is serving everyone else correctly.

# Which user agents produce 499s?
awk '$2 == 499' /var/log/nginx/access.log \
  | grep -oE 'ua="[^"]*"' | sort | uniq -c | sort -rn | head -10

If one agent dominates, fix the checker rather than the server — or fix the endpoint if the checker's expectation is reasonable and your response time is not.

The setting that makes 499 disappear, and why that is rarely the answer

Nginx offers proxy_ignore_client_abort, off by default. Left off, nginx stops the upstream request when the client disconnects, which frees the worker and produces the 499 log line. Turned on, nginx carries the request through to completion regardless.

Turning it on removes the log entries. It does not remove the abandonment — the visitor still left — and it spends resources finishing work nobody will receive. Under load that is exactly backwards, because the abandoned requests are usually the slow ones and finishing them competes with the requests still being waited for.

There is one situation where it is the right call: requests with side effects. A write that is cut off halfway can leave partial state, and completing it is safer than aborting it.

# Right: protect a mutating endpoint from a half-applied write
location /api/checkout {
    proxy_ignore_client_abort on;
    proxy_pass http://app_backend;
}

# Wrong as a global setting: hides the signal and burns workers on
# responses that will never be delivered

If the motivation for enabling it is "our 499 count looks bad", the change is cosmetic and costly. The count is the measurement, not the problem — removing the measurement leaves the abandonment and takes away the warning.

Why the count is worth watching

Clients stop waiting before servers stop working. That ordering makes 499 an early signal in a way 5xx codes are not: by the time an upstream returns 502 or 504, the degradation has already been running long enough to exhaust a server-side limit.

A useful alert therefore watches the rate and its shape, not the absolute number. Two patterns are worth waking up for:

  • A step change — the rate roughly doubles and stays there. Something changed: a deploy, a dependency, a traffic mix.
  • Clustering on one duration — values converging on a single number means a timeout is now being hit routinely that previously was not.

Neither pattern is visible in a single log inspection; both are obvious in a series. That is the argument for recording status codes and response times continuously rather than reading logs after a complaint. Scheduled monitoring keeps that series, and the header checker confirms from outside what a real client receives when the endpoint is healthy.

Timeline chart where a rising band of early abandonment precedes a later spike of server errors
Abandonment rises first because clients give up before server-side limits are reached. The later spike is the same event, noticed late.

A working order for investigating a 499 spike

  1. Add the timing variables to the log format if they are not there. Without $request_time every 499 looks the same and none of the following steps work.
  2. Plot the durations. Clustered on a round number means a timeout; scattered means slowness; sub-second means normal behaviour.
  3. If clustered, find the layer whose configured timeout equals that number. That layer is severing the connection.
  4. Check the ordering of every timeout in the path and make the innermost the shortest.
  5. If scattered, find the endpoints and compare against the slowest completed requests. The overlap is your target.
  6. Attribute the remainder to user agents. A dominant checker or crawler is a configuration conversation, not an outage.

How to check your site right now

Run the speed check against the endpoints that produce 499s and read the timing breakdown — if the gap to first byte is large, you have found the slow upstream without touching a log. Use the header checker to confirm what a healthy response actually looks like, and uptime monitoring to record response time continuously, since a 499 spike is a change over time and a single check cannot show a change.

Because 499 is written by nginx and by edges built on it, the same pattern appears in CDN logs. When the number that 499s cluster on matches an edge's own limit rather than your server's, the layer to fix is the edge — the origin-side error family is covered in the origin error guide.

Histogram of request durations with a tall narrow column at a single round number and a low wide spread elsewhere
Shape carries the diagnosis. A spike at one duration is a configured limit; a wide spread is people losing patience.

Frequently asked questions

Is 499 an error I should fix?

It depends entirely on the duration. Sub-second entries are normal client behaviour. Entries clustered on a round number are a timeout misconfiguration worth fixing. Long scattered entries mean the page is too slow and people are leaving.

Do users see a 499 page?

No. The connection is already gone when nginx writes the line, so there is nothing to send it to. Whatever the user saw came from their own browser or from an intermediate proxy.

Should I enable proxy_ignore_client_abort?

Only for endpoints where an interrupted request could leave inconsistent state. As a global setting it hides the signal, keeps workers busy producing responses nobody receives, and makes behaviour under load worse rather than better.

Why do 499s appear in Cloudflare logs too?

Because the edge is built on nginx and inherits the convention. When they appear there, the client that gave up may be the browser or may be the edge's own connection to your origin — check whether the duration matches the edge's limit or your own.

My 499 count jumped after a deploy. What changed?

Most likely a response got slower and is now crossing a limit that it previously fitted inside. Compare the duration distribution before and after: if the values now pile up on a number they did not reach before, you have both the cause and the layer.

Apache does not log 499. Does that mean it does not happen?

It happens identically; Apache just has no code for it. Clients abandon requests on every server. Nginx chose to record the event, which is an advantage rather than a defect — you can see something other servers leave invisible.

Checklist

  • Log $request_time and $upstream_response_time — without them 499 is undiagnosable.
  • Look at the distribution of durations, not the count.
  • Clustering on a round number means a configured timeout; find which layer owns it.
  • Order timeouts so the innermost is shortest and each outer layer allows more.
  • Compare 499 endpoints against the slowest completed requests.
  • Attribute short 499s to navigation and cancelled requests before treating them as faults.
  • Check user agents for a health checker with an unrealistic timeout.
  • Use proxy_ignore_client_abort only where a partial write would be harmful.
  • Alert on rate change and shape, not on an absolute threshold.
  • Treat a rising 499 rate as early warning, not as noise.

Check your website right now

Check your site's HTTP status →
More articles: HTTP
HTTP
404 Not Found Error: Causes and Fixes for Chrome, nginx, Apache
15.04.2026 · 1 967 views
HTTP
HTTP Methods Explained: GET, POST, PUT, DELETE and Beyond
16.03.2026 · 978 views
HTTP
Server-Sent Events vs WebSockets: Which to Use for Realtime
16.03.2026 · 974 views
HTTP
The Complete HTTP Request Lifecycle: From URL to Rendered Page
16.03.2026 · 892 views