Introducing Highlights: the context that matters

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

Ship an agent that actually knows things.

Free tier, 10-minute integration, and the same API powering agents at Mintlify, daily.dev, and Propane. No credit card to start.