Introducing Highlights: the context that matters

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: close with 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

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.