499 Client Closed Request (tested)

499 Client Closed Request is an nginx log code. It means the client closed the connection before nginx sent a response. It is never sent to the client. We reproduced the same situation against a Node server to show what the backend sees and how to stop wasted work.

1. What 499 means

499 is not a real HTTP status. Only nginx uses it, and only in its access log. nginx writes it when the client disconnects while nginx is still waiting for the upstream or still working. No client ever receives a 499, because by then nobody is listening.

So a 499 is a timeout on the client side. The client got tired first. Compare 504 Gateway Timeout, where nginx gives up on the upstream, and 408 Request Timeout, where the server gives up on a slow client.

2. Our test setup

We have no nginx on the test box, so we measured the backend's view instead. A Node.js server ran a handler that does 5 s of work (50 ticks of 100 ms) and then sends 200. Four clients each gave up after 1 s. The server logged req and res 'close' events and res.writableFinished.

res.on('close', () => {
  if (!res.writableFinished) console.log('client aborted');
});

3. Results: every client abort looks the same to the server

ClientClient sawServer 'close' afterwritableFinishedHandler kept running?
curl --max-time 1exit 28, "Operation timed out after 1000 milliseconds with 0 bytes received", %{http_code} = 0001002 msfalseYes, finished at 5011 ms
Node fetch + AbortSignal.timeout(1000)TimeoutError after 1012 ms969 msfalseYes, finished at 5013 ms
Python urllib timeout=1TimeoutError "timed out" after 1015 ms1002 msfalseYes, finished at 5021 ms
Chromium page navigated away after 1 snet::ERR_ABORTED1000 msfalseYes, finished at 5010 ms
Chromium fetch + AbortController.abort() at 1 sAbortError after 1003 ms1009 msfalseNo (handler checked the signal)

In every case the server got res 'close' and req 'close' within about 10 ms of the client giving up. res.writableFinished was false every time. That is the Node equivalent of the 499 moment.

Without a check, the handler did all 5 s of work anyway. When it finally called res.end(), the socket was already destroyed (res.destroyed was true). The 200 it "would have sent" went nowhere. curl shows that clearly: the status is 000 and 0 bytes were received.

$ curl -sS -m 1 -w '%{http_code}\n' http://127.0.0.1:8983/slow
curl: (28) Operation timed out after 1000 milliseconds with 0 bytes received
000

4. Stop the wasted work

Turn the close event into an AbortSignal and check it in your work loop, or pass it to fetch and database calls that accept a signal.

http.createServer((req, res) => {
  const ac = new AbortController();
  res.on('close', () => {
    if (!res.writableFinished) ac.abort();   // client went away
  });
  doSlowWork({ signal: ac.signal })
    .then(body => res.end(body))
    .catch(err => { if (!ac.signal.aborted) throw err; });
});

With this guard, our curl test logged "work stopped after 9 ticks" 3 ms after the close. The Chromium fetch abort stopped at 9 ticks as well. That is about 0.9 s of work instead of 5 s.

Use res.on('close') with the writableFinished check. A plain req.on('close') also fires after a normal, finished response, so it alone does not prove an abort.

5. nginx specifics (from the docs, not measured)

When the client closes, nginx logs 499 and by default closes its connection to the upstream too. Your backend then sees the same close event we measured above.

proxy_ignore_client_abort on; tells nginx to keep the upstream request open after the client leaves. Use it for requests that must finish (for example a payment write). It does not help the client; the client is gone either way.

location /api/ {
    proxy_pass http://backend;
    proxy_read_timeout 60s;
    # proxy_ignore_client_abort on;  # only if the work must complete
}

6. Common causes

7. How to fix it

If you see resets rather than clean closes, read ERR_CONNECTION_RESET / ECONNRESET. If the upstream itself fails, see 502 Bad Gateway.

Frequently asked questions

Can a client ever receive a 499 status?

No. nginx only writes 499 to its access log. The client has already closed the connection, so nothing is sent. In our tests curl reported status 000 with 0 bytes received.

Is 499 an error in my server?

Not by itself. It means the client gave up first. A few are normal (users leaving, cancelled fetches). Many 499s on one endpoint usually mean that endpoint is slower than some client or load balancer timeout.

Does my backend keep working after a 499?

Yes, unless you check. In our Node test the handler ran the full 5 s after every client left at 1 s. Listen for res 'close' with writableFinished false and abort the work.

What does proxy_ignore_client_abort do?

It tells nginx not to close the upstream connection when the client leaves, so the backend can finish. Per the nginx docs it is off by default. Use it only for work that must complete.

What is the difference between 499 and 504?

499: the client gave up waiting for nginx. 504: nginx gave up waiting for the upstream. If the client timeout is shorter than proxy_read_timeout you get 499; if it is longer you get 504.

Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0, curl 8.14.1, Python 3 urllib and headless Chromium 153 against a Node server whose handler takes 5 s. Each client gave up after 1 s. The nginx parts are from the nginx docs, not measured. PlainHeaders is our extension; curl, Python, Node.js and Chromium are free.