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.

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 499 | What happened | Owner |
|---|---|---|
| Close to a round number — 30 s, 60 s, 100 s | Something timed out on a fixed limit | Timeout configuration |
| Long but scattered | Humans gave up waiting at their own pace | Application performance |
| Very short — well under a second | Navigation away, cancelled fetch, scanner | Usually nothing to fix |
| Short and from one user agent | A health check or bot with a tight timeout | The 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.
| Layer | Typical default | Should be |
|---|---|---|
| Application worker | often 30 s | Shortest — fails first, returns an error |
nginx proxy_read_timeout | 60 s | Slightly longer than the application |
| Load balancer / CDN | 60–100 s | Longer than nginx |
| Client / browser | varies, often longer | Longest |
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.

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.

A working order for investigating a 499 spike
- Add the timing variables to the log format if they are not there. Without
$request_timeevery 499 looks the same and none of the following steps work. - Plot the durations. Clustered on a round number means a timeout; scattered means slowness; sub-second means normal behaviour.
- If clustered, find the layer whose configured timeout equals that number. That layer is severing the connection.
- Check the ordering of every timeout in the path and make the innermost the shortest.
- If scattered, find the endpoints and compare against the slowest completed requests. The overlap is your target.
- 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.

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_timeand$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_abortonly 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.