Nginx returns 499 on slow client uploads because client_body_timeout fires before the proxy reads the body
Nginx returns 499 on slow client uploads because client_body_timeout fires before the proxy reads the body
A phone upload of a 40 MB video stalls at 70%. Nginx writes 499 to the access log, the app shows a generic failure, and the upstream service never receives a complete body. The user retries on the same weak connection, the support ticket grows, and the backend gets another partial request.
That pattern usually starts with client_body_timeout, not with the upstream app.
499 is a symptom, not the upload result
Nginx logs 499 when the client closes the connection before Nginx sends a response [link needed]. On uploads, that often means the client gave up while Nginx was still waiting for the next chunk of the request body.
client_body_timeout applies between successive reads from the client body; it is not a total upload deadline [link needed]. A slow but steady client can take minutes without tripping it. A client that goes quiet for longer than the configured gap can trip it.
With proxy_request_buffering on, Nginx buffers the request body before forwarding it to the proxied server [link needed]. If the body read times out first, the proxy never gets a complete body. You see a failed upload and a 499, while the upstream may see nothing or a truncated request.
Diagnose nginx 499 client_body_timeout before you raise limits
Treat nginx 499 client_body_timeout as a per-read gap problem, not a “make uploads faster” problem. Run this triage:
1. Reproduce with a throttled upload and capture logs:
```bash
curl --limit-rate 20k -T large.bin https://example.com/upload -v
```
Confirm the access log shows 499 and the error log shows a timeout while reading the client request body [link needed].
2. Inspect the effective config:
```bash
nginx -T | grep -E 'client_body_timeout|proxy_request_buffering|client_max_body_size'
```
Do not assume the value in one file is the value Nginx uses.
3. Compare the timeout to the worst-case gap between reads, not the total upload time. If buffering is on, Nginx must finish reading the body before proxy_pass [link needed].
4. In staging, set a location-specific value:
```nginx
location /upload/ {
client_body_timeout 120s;
proxy_request_buffering off; # only if the upstream can stream
proxy_pass http://app;
}
```
5. Re-run the throttled upload. Verify the upstream receives the full body, then reload:
```bash
nginx -t && nginx -s reload
```
Fix the timeout and buffering pair
Raise client_body_timeout only where large or slow uploads land. A global jump from the default 60s to several minutes broadens exposure to slow clients holding connections [link needed]. A location-scoped value is easier to reason about.
If the upstream can consume a streamed body, proxy_request_buffering off lets Nginx forward bytes as they arrive instead of waiting for the full body [link needed]. It does not remove client_body_timeout; it changes when the upstream sees data. Keep buffering on if the upstream is slow, needs a complete body, or cannot handle chunked requests well.
Check adjacent limits too: client_max_body_size controls size, not time; proxy_read_timeout controls upstream response reads, not client uploads; send_timeout controls writes back to the client.
When NOT to do this
Do not disable proxy_request_buffering globally just because one upload route is slow. Do not set client_body_timeout to hours unless your slow-client risk is acceptable. Do not tune timeouts when the 499 is actually a user cancel, a mobile network drop, or a client-side timeout that happens before Nginx’s body timeout. And do not raise the timeout if the real failure is an upstream that reads too slowly and creates backpressure.
Checklist
- Confirm
499plus an error-log body-read timeout. - Measure the read gap, not total upload duration.
- Set
client_body_timeouton the upload location. - Test with
curl --limit-ratebefore rollout. - Keep request buffering on unless the upstream streams.
- Reload with
nginx -tand watch499s after deploy.