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.
| Where | What you see |
|---|---|
| curl -f | curl: (22) The requested URL returned error: 408 |
| Python urllib | urllib.error.HTTPError: HTTP Error 408: Request Timeout |
| Python urllib, upload cut off midway | urllib.error.URLError: <urlopen error [Errno 32] Broken pipe> |
| Node.js fetch() (res.status, res.statusText) | 408 Request Timeout |
| Chrome desktop, 408 with an empty body | This page isn't working ... HTTP ERROR 408 |
| HAProxy default error page | 408 Request Time-out Your browser didn't send a complete request in time. |
| Apache httpd default error page | Request 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.
- 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]) - 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.
Build or download the full payload before you open the request.
Upload from a file on disk, not from a generator waiting on other work.
Use the API's multipart or resumable upload for files that take minutes.
- 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.
nginx: raise client_body_timeout in the upload location; it measures pauses between reads.
Node.js: raise server.requestTimeout, the limit for receiving the whole request.
Apache: raise the body limit in RequestReadTimeout; its MinRate already adds time while data flows.
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.
Send a Keep-Alive timeout header that matches your real idle limit, so clients that read it close first.
Log the method, body size and elapsed time with every 408, so upload timeouts stand apart from idle ones.
Enable HAProxy's option http-ignore-probes only if preconnect 408s flood your logs, since it also hides real failures.
Before launch, upload your largest allowed file with curl --limit-rate to learn which limit fires first.
Prefer HTTP/2 for busy APIs; its GOAWAY frame tells clients which requests the server never processed.
Related errors
Learn more
FAQ
Retry on the same port
Thousands of requests can run on one Geonode port, so retries need no new port.


