Geonode logo
Website and server errors

How to Fix a 408 Request Timeout Error

Tell an idle-connection 408 from a stalled upload, then fix it, with cURL, Python and Node.js retry code.

Updated

TL;DR

A 408 Request Timeout comes from the website's server: your full request did not arrive within its time limit, so it closed the connection. Retry a safe-to-repeat request once on a new connection, and keep upload bodies flowing without long pauses.

What a 408 looks like in each client

Exact strings from our local runs and from HAProxy's and Chrome's source code.

WhereWhat you see
curl -fcurl: (22) The requested URL returned error: 408
Python urlliburllib.error.HTTPError: HTTP Error 408: Request Timeout
Python urllib, upload cut off midwayurllib.error.URLError: <urlopen error [Errno 32] Broken pipe>
Node.js fetch() (res.status, res.statusText)408 Request Timeout
Chrome desktop, 408 with an empty bodyThis page isn't working ... HTTP ERROR 408
HAProxy default error page408 Request Time-out Your browser didn't send a complete request in time.
Apache httpd default error pageRequest Timeout Server timeout waiting for the HTTP request from the client.

Why this happens

The website stopped waiting for the rest of your request and hung up.

Often an upload pauses or crawls past the server's limit. Some servers also send 408 on an idle kept-open connection, and a client that reuses it reads that 408 as its answer.

Diagnose your 408 first

Find out whether an upload, a reused connection or a server limit caused it.

  • Note what failed; if only uploads or long form posts get 408 while pages load, the server gave up waiting for the body.

  • Time two failing uploads; if both stop after about the same time, a fixed server limit fired.

  • Run curl -v through your proxy; a 200 reply to CONNECT followed by the 408 means the website answered, not the proxy.

  • In Node.js, log req.reusedSocket with the status; true on a 408 means the server closed that idle connection as you reused it.

  • On your own nginx, grep the access log for 408; nginx records header and body timeouts there, yet sends clients nothing before closing.

Solutions ranked by effectiveness

Retry first when 408s come and go; uploads and server limits come next.

  1. Most common fix

    Retry safe requests once on a new connection

    Applies when 408s come and go. RFC 9110 lets a client repeat a timed-out request, so resend GET and other repeatable requests once, after a short pause.

    import time, urllib.error, urllib.request
    def get(url, tries=2):
        for attempt in range(1, tries + 1):
            try:
                with urllib.request.urlopen(url, timeout=60) as res:
                    return res.status, res.read()
            except urllib.error.HTTPError as err:
                if err.code != 408 or attempt == tries:
                    raise
                time.sleep(1)
    
    print(get("https://example.com/api/items")[0])
  2. Check next

    Keep upload bodies moving without pauses

    Applies when uploads or slowly built bodies fail. Servers time the gaps between reads or the whole request, so a body that waits on other work can run out the clock.

    1. Build or download the full payload before you open the request.

    2. Upload from a file on disk, not from a generator waiting on other work.

    3. Use the API's multipart or resumable upload for files that take minutes.

  3. Site owners

    Fit server timeouts to real uploads

    Applies when your own server cuts off genuine uploads. Raise limits only on upload routes; the same timers close connections that never send anything.

    1. nginx: raise client_body_timeout in the upload location; it measures pauses between reads.

    2. Node.js: raise server.requestTimeout, the limit for receiving the whole request.

    3. Apache: raise the body limit in RequestReadTimeout; its MinRate already adds time while data flows.

    4. AWS ALB: set the idle timeout above the longest pause an upload may have.

Stop the 408 from coming back

Habits that keep idle-connection 408s rare and upload timeouts easy to spot.

  1. Send a Keep-Alive timeout header that matches your real idle limit, so clients that read it close first.

  2. Log the method, body size and elapsed time with every 408, so upload timeouts stand apart from idle ones.

  3. Enable HAProxy's option http-ignore-probes only if preconnect 408s flood your logs, since it also hides real failures.

  4. Before launch, upload your largest allowed file with curl --limit-rate to learn which limit fires first.

  5. Prefer HTTP/2 for busy APIs; its GOAWAY frame tells clients which requests the server never processed.

Check bandwidth from your scriptsGeonode's usage statistics API returns the bandwidth your proxy account has used, in bytes.
Try residential proxies

Related errors

Learn more

FAQ

It means the website's server did not receive your complete request within the time it was prepared to wait, so it closed the connection without acting on the request.

Not by Geonode's gateway: its documented codes are 403, 407, 411, 464 to 468, 500, 517, 518, 560 and 561. A 408 through it comes from the website or a load balancer in front of it.

A 408 means the site's server ran out of patience with your request. A 504 means your request arrived whole, but a gateway in front of the site timed out waiting on the site's own server.

Browsers open connections in advance and may never use them; servers such as HAProxy answer those with 408. Chrome resends a request that meets a 408 on a reused HTTP/1.1 connection instead of showing the error.

The server replied while your client was still pushing the body, so the client hit the closed connection first. In our tests Python's urllib reported Broken pipe; curl printed the status, then Connection reset by peer.

It depends on the server: nginx allows 60 seconds for headers and 60 between body reads, Node.js 18 and later 300 seconds for the whole request, Apache 20 seconds for the body, extended while data arrives.

Retry on the same port

Thousands of requests can run on one Geonode port, so retries need no new port.