HTTP 408 Request Timeout: What It Means and How to Fix It
HTTP 408 Request Timeout is a status code a server sends when it did not receive the complete request within the time it was prepared to wait, and it usually closes the connection afterwards. RFC 9110 lets the client repeat the request, so a 408 on an idempotent GET is safe to retry on a new connection.
- Code
- 408
- Name
- Request Timeout
- Class
- 4xx client error
- Retry?
- Yes, with backoff
What causes a 408 error?
- →A slow or congested network between client and server.
- →A large upload that the client sends slower than the server allows.
- →Idle keep-alive connections that the server closes while the client still plans to reuse them.
- →A client that opens a connection and waits too long before sending the request.
How do you fix a 408 error when web scraping?
- →Retry on a fresh connection with exponential backoff and jitter.
- →Close idle pooled connections sooner than the server does, so you do not reuse a connection it is about to drop.
- →Reduce concurrency if 408s rise with load. Your own bandwidth may be the bottleneck.
How do you fix a 408 error on your own server?
- →Tune the request header and body timeouts for your slowest legitimate clients.
- →Send
Connection: closewith the 408, as RFC 9110 recommends. - →Accept large uploads through resumable or direct-to-storage flows rather than one long request.
How do you handle a 408 error in a retry loop?
fetch() treats 408 as temporary. It waits for Retry-After when the server sends a number of seconds, otherwise backs off exponentially with jitter, caps every wait at 60 seconds, and gives up after five attempts.
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 408 error?
The Context.dev API returns 408 when a request reaches its deadline with timeoutOpts.behavior set to fail. The default deadline is 90,000 ms and the maximum is 300,000 ms; with return-partial (at least 5,000 ms) you get whatever outputs are ready instead. Keep your client timeout longer than the API deadline. The Context.dev SDKs retry twice with exponential backoff after connection errors and 408, 409, 429, and 5xx responses from the API itself.
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 408 error
Is it safe to retry a 408?
Yes for idempotent requests such as GET. RFC 9110 explicitly allows the client to repeat the request, on a new connection if the old one was closed.
What is the difference between 408 and 504?
A 408 means the server waited too long for your request. A 504 means a gateway waited too long for the upstream server’s response.
Why do I get 408 errors on keep-alive connections?
The server closed an idle connection while your client still had it in its pool. Lower the client’s idle timeout below the server’s.
Which status codes are related to 408?
Sources
Last reviewed