408 Request Timeout (tested)
408 Request Timeout means the server gave up waiting for the client to finish sending its request. It is not a slow-backend error (that is 504), and it is not what you see when your own client times out (that is an exception, not a status code). We made Node.js 24 send real 408s, checked which clients retry them, and measured what client-side timeouts look like instead.
1. What makes Node.js send a 408
Node.js 24.21.0 has two request timers. headersTimeout (default 60000 ms) limits how long the request headers may take; that case is measured in our keep-alive guide. requestTimeout (default 300000 ms) limits the whole request, headers and body. We set it low and sent valid headers with Content-Length: 100, then one body byte every 400 ms from a raw TCP socket:
const server = http.createServer(
{ requestTimeout: 3000, connectionsCheckingInterval: 500 },
handler);
| Client behaviour | Result | After |
|---|---|---|
| Headers sent, body trickled 1 byte / 400 ms (8 of 100 bytes arrived) | HTTP/1.1 408 Request Timeout + Connection: close, socket closed | 3,012 ms |
| Connected, sent nothing at all | 408 Request Timeout + Connection: close | 3,501 ms |
| One complete request, then idle on the keep-alive socket | Normal 200, then closed silently (no 408) | 6,004 ms |
The full 408 response was just the status line and Connection: close, no body. Your handler never ran, so your logging middleware never saw these requests.
Two details we hit:
- Setting
requestTimeout: 3000also loweredserver.headersTimeoutto 3000 (we read it back from the server object). Node.js keepsheadersTimeoutat or belowrequestTimeout, which is why the "sent nothing" connection also got a 408. - An idle connection between requests is governed by
keepAliveTimeout, not these timers, and is closed without any response. A 408 only goes out while a request is in progress (or before the first one).
With the defaults, a client uploading a large body over a slow link gets a 408 after 5 minutes. If you accept big uploads, raise requestTimeout (or set it to 0 and enforce limits elsewhere) and cap size separately; see 413 Request Entity Too Large.
2. Do clients retry a 408?
A test server answered every request with 408 and Connection: close and counted hits:
| Client | Status seen | Requests sent |
|---|---|---|
| curl 8.14.1 | 408 | 1 |
curl 8.14.1 --retry 3 --retry-delay 1 | 408 | 4 (curl treats 408 as transient) |
Node.js 24 fetch() | 408 | 1 |
Chromium 153, page.goto | 408 | 1 |
Chromium 153, fetch() inside the page | 408 | 1 |
From the Chromium source, not measured: Chromium silently retries a request when a 408 arrives on a reused keep-alive connection, treating it like a server that closed the idle socket. On a fresh connection, as in our test, it shows you the 408. Never assume a retry happened: if your API client should retry, add it yourself, and only for idempotent requests (GET, PUT, DELETE) or with an idempotency key.
3. 408 vs 504 vs client-side timeouts
| What you see | Who gave up | Waiting for |
|---|---|---|
408 Request Timeout | The server | The client to finish sending the request |
504 Gateway Timeout | A proxy or load balancer | The upstream app to respond |
curl (28) Operation timed out after 1001 milliseconds with 0 bytes received, exit code 28 | Your client (--max-time 1) | The response |
Node.js fetch throws TimeoutError: The operation was aborted due to timeout | Your client (AbortSignal.timeout(1000)) | The response |
The last two rows are measured against a handler that waited 5 s before answering. No status code exists in either case: the client abandoned the request. If your logs show 408s, look at the client side (slow uploads, stalled mobile networks, a client that sends Content-Length larger than the body it actually sends). If they show 504s, look at your app's response time.
4. nginx (from the docs, not measured)
nginx returns 408 when client_header_timeout (default 60s) or client_body_timeout (default 60s) expires. The body timeout is measured between two successive reads, not for the whole body, so a slow but steady upload does not trigger it. proxy_read_timeout (default 60s) is the one that produces 504 when your upstream is slow. Clients that send Expect: 100-continue wait before sending a body; see Expect: 100-continue.
5. Fixing it
- Uploads cut off at exactly 5 minutes on Node.js: raise
server.requestTimeout. - 408s on a mostly idle service: often clients that open a connection and never send a request (pre-connects, health checkers). Usually harmless.
- Behind a proxy: the 408 may come from the proxy, not your app. Check which component's timeout matches the delay you see.
- Client code: handle 408 like a connection error: retry once, with a fresh connection, for idempotent requests.
Frequently asked questions
What does 408 Request Timeout mean?
The server stopped waiting for the client to finish sending its request. Node.js 24 with requestTimeout 3000 sent 408 with Connection: close at 3,012 ms to a client that sent headers and then trickled the body one byte every 400 ms.
Is 408 a client or server problem?
It is a 4xx code, so the server blames the client: the request arrived too slowly or not at all. A slow backend shows up as 504 Gateway Timeout from a proxy instead.
Does the browser or curl retry a 408 automatically?
In our test, no: curl 8.14.1, Node.js 24 fetch and Chromium 153 each sent 1 request and showed the 408. curl --retry 3 retried it, sending 4 requests. From Chromium's source, not measured: it retries silently when the 408 arrives on a reused keep-alive connection.
What is the default request timeout in Node.js?
server.requestTimeout is 300000 ms (5 minutes) and server.headersTimeout is 60000 ms in Node.js 24.21.0. Setting requestTimeout lower also lowered headersTimeout to the same value.
Is a client-side timeout a 408?
No. curl --max-time ends with error 28 and exit code 28, and fetch with AbortSignal.timeout throws a TimeoutError. No status code is received at all.
Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0 (http.createServer on localhost:8987-8988, a raw TCP client, built-in fetch), curl 8.14.1 and headless Chromium 153.0.8010.12 (Playwright). We counted server hits to detect retries. nginx timeouts and Chromium's keep-alive retry rule are quoted from documentation and source, not measured. PlainHeaders is our extension; Node.js, curl and Chromium are free.