HTTP 499 Client Closed Request: What It Means and How to Fix It
HTTP 499 Client Closed Request is a non-standard status code that nginx writes to its access log when the client closes the connection before the server has sent a response. The client never receives a 499. If you see it in your logs, a caller timed out or gave up, often because the upstream application was slow.
- Code
- 499
- Name
- Client Closed Request
- Class
- 4xx client error
- Retry?
- After your own timeout
What causes a 499 error?
- →A client timeout shorter than the time the server needs to respond.
- →A user who navigated away or a script that cancelled the request.
- →A load balancer or proxy in front of nginx with a shorter timeout than nginx’s upstream.
- →Slow upstream applications that hold requests open for a long time.
How do you fix a 499 error when web scraping?
- →Raise your client read timeout for slow pages, and keep connect timeouts short.
- →Retry with backoff after your own timeout instead of hammering the server with immediate repeats.
- →Lower concurrency against slow sites. Parallel requests make each one slower.
How do you fix a 499 error on your own server?
- →Find the slow upstream requests in the logs around the 499s and fix those first.
- →Align timeouts along the chain: client, load balancer, nginx, and application.
- →Move long work to background jobs and let clients poll for the result.
How do you handle a 499 error in a retry loop?
Your client never receives a 499. It sees its own timeout instead. fetch() catches requests.Timeout, backs off with jitter, and tries again. Give the read timeout enough room for slow pages so the server does not log a 499 for every request you abandon.
import random
import time
import requests
RETRYABLE = {408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524}
def fetch(url: str, max_attempts: int = 5) -> requests.Response:
for attempt in range(max_attempts):
try:
response = requests.get(url, timeout=(10, 60))
except requests.Timeout:
time.sleep(2**attempt + random.uniform(0, 1))
continue
if response.status_code not in RETRYABLE:
response.raise_for_status()
return response
retry_after = response.headers.get("Retry-After", "")
backoff = 2**attempt + random.uniform(0, 1)
time.sleep(min(int(retry_after) if retry_after.isdigit() else backoff, 60))
raise RuntimeError(f"Gave up on {url} after {max_attempts} attempts")
How does Context.dev handle a 499 error?
Context.dev documents a request deadline (90,000 ms by default, up to 300,000 ms) and recommends keeping your client timeout longer than that deadline, so your client does not hang up on a request that is still running.
See what the web scraping API does on every request, or read how to fix HTTP errors in web scraping for a longer walkthrough.
Frequently asked questions about a 499 error
Is 499 a real HTTP status code?
Not in any RFC. It is nginx’s own code for logging a request the client abandoned. Clients never see it in a response.
What is the difference between 499 and 504?
A 499 means the client gave up waiting. A 504 means a gateway gave up waiting on the upstream server and told the client.
How do I reduce 499 errors?
Make slow endpoints faster, or give clients a longer timeout and a polling pattern for long jobs.
Which status codes are related to 499?
Sources
Last reviewed