Skip to main content
408

Request Timeout

Active
RFC 9110 §15.5.9Since 1997Slow networks, file uploads, keep-alive connections, API gateways

HTTP 408 Request Timeout indicates the server did not receive a complete request message within the time it was prepared to wait. Defined in RFC 9110 §15.5.9. The server closes the connection without waiting for the request to finish. The client MAY repeat the request without modifications since the timeout is not the request's fault.

Description

The 408 Request Timeout status code means the server did not receive a complete request within its configured time limit. This is a server-initiated timeout — the server decided the client is taking too long to send the full request (headers and/or body), so it gives up and closes the connection.

408 is fundamentally different from 504 Gateway Timeout. 504 means a proxy or gateway didn't get a timely response from an upstream server. 408 means the origin server itself is tired of waiting for the CLIENT to finish transmitting. The request never arrived completely — it's not that processing took too long, it's that the bytes never showed up.

The server SHOULD send the Connection: close header along with 408, signaling that it will shut down the connection. Per the RFC, the server MAY close the connection even if it hasn't received the complete request. The client can retry the same request without modifications — the timeout isn't a fault of the request content, it's a transmission timing issue.

Common causes include slow clients on poor connections, clients that open a connection and send headers but never transmit the body, keep-alive connections that go idle too long, and network interruptions mid-transmission. Servers like Nginx (client_header_timeout: 60s, client_body_timeout: 60s), Apache (Timeout: 300s, RequestReadTimeout), and Cloudflare (100s connection timeout) all enforce these limits to protect resources from being tied up indefinitely.

In practice, browsers rarely surface 408 to users because the connection often drops before the response arrives. The status code is more commonly seen in API clients and server logs. Some servers also use 408 preemptively on idle keep-alive connections to reclaim resources, which is technically valid but can surprise clients that expected the connection to remain open.

Examples

Client sends headers but body never arrives
http
POST /api/upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
Content-Length: 52428800
Authorization: Bearer token123

------WebKitFormBoundary
[... client stalls here, body never completes ...]
408 response — server gives up waiting
http
HTTP/1.1 408 Request Timeout
Content-Type: application/json
Connection: close

{"error": "request_timeout", "message": "The server did not receive a complete request within the allotted time. Please retry.", "timeout_seconds": 60}
Idle keep-alive connection timeout
http
GET /api/status HTTP/1.1
Host: api.example.com
Connection: keep-alive

[... connection sits idle for 120 seconds ...]
408 response — keep-alive reclaimed
http
HTTP/1.1 408 Request Timeout
Connection: close
Content-Length: 0
Slow header transmission — drip attack pattern
http
GET /api/data HTTP/1.1

Host: api.example.com

[... 1 byte every 30 seconds ...]
Accept: application/json

[... server timeout reached before headers complete ...]
408 response with retry guidance
http
HTTP/1.1 408 Request Timeout
Content-Type: application/json
Connection: close
Retry-After: 30

{"error": "request_timeout", "message": "Request headers were not received within 60 seconds.", "retry_after": 30, "documentation_url": "https://api.example.com/docs/timeouts"}

Edge Cases

  • 408 is a SERVER-side decision about CLIENT speed — not about server processing time. If the server received the full request but took too long to process it, that's not a 408 situation (no standard code exists for that; some use 503 or 504 behind a proxy).
  • Browsers often never display 408 because the TCP connection drops before the response can be delivered. The client may see a network error or ERR_CONNECTION_RESET instead of a clean 408 response.
  • Nginx uses 408 for client_header_timeout and client_body_timeout violations. If a client sends headers within the limit but then stalls on the body, Nginx returns 408 after client_body_timeout expires — even if some body bytes were received.
  • Some servers (notably Nginx) send 408 on idle keep-alive connections to reclaim resources. This is technically valid but can trigger retry storms if clients treat every 408 as 'retry immediately'. Implement exponential backoff.
  • The Slowloris attack exploits this exact mechanism — sending partial headers very slowly to keep connections open. Servers defend with aggressive client_header_timeout values and connection limits per IP. Apache's mod_reqtimeout was created specifically for this.
  • 408 should include Connection: close because the request was never completed — there's no clean way to continue on the same connection. The server doesn't know where the next request boundary would be in the byte stream.
  • Cloudflare returns 408 after 100 seconds of connection idle time, but this is measured at the edge — the origin server's own timeout may be different. Clients behind Cloudflare see Cloudflare's timeout, not the origin's.
  • Unlike 401 or 403, a 408 explicitly means the client can retry without modification. The request content isn't wrong — it just didn't arrive in time. Retry with the same headers, same body, same everything.

When You'll See This

  • Mobile client on a flaky cellular connection loses signal mid-upload
  • Large file upload stalls because the client's upstream bandwidth is saturated
  • Slowloris-style attack sending partial HTTP headers at 1 byte per second
  • Keep-alive connection sitting idle beyond the server's configured timeout
  • Client opens connection and sends headers but crashes before sending the body
  • Load balancer health check connection that opens but never sends a request
  • WebSocket upgrade request that never completes the handshake within the timeout window
  • API client with a misconfigured Content-Length that promises more bytes than it sends

Implementation References

LanguageConstant
Gohttp.StatusRequestTimeout
Rusthttp::StatusCode::REQUEST_TIMEOUT
Pythonhttp.HTTPStatus.REQUEST_TIMEOUT
Node.jshttp.STATUS_CODES[408]
.NETHttpStatusCode.RequestTimeout
JavaHttpURLConnection.HTTP_CLIENT_TIMEOUT
PHPResponse::HTTP_REQUEST_TIMEOUT (Symfony)
Ruby:request_timeout (Rack)

History

Introduced in HTTP/1.1 (RFC 2068, January 1997) alongside the persistent connection model that made idle-connection timeouts necessary. Carried forward unchanged through RFC 2616 (1999) and RFC 7231 (2014). Refined in RFC 9110 (June 2022) with clearer language that the server MAY close the connection without waiting for the request to complete. Became operationally significant with the Slowloris attack disclosure (2009) which exploited slow-header transmission to exhaust server connections.

Related Status Codes

Related Headers

FAQ

What is the difference between 408 Request Timeout and 504 Gateway Timeout?

408 is sent by the origin server when the CLIENT is too slow sending the request. The server gave up waiting for the full request to arrive. 504 is sent by a PROXY or gateway when the UPSTREAM server is too slow responding. The proxy received the client's request fine but couldn't get an answer from the backend in time. Think of it as: 408 = 'you (client) are too slow talking to me', 504 = 'the server behind me is too slow answering'.

Can I safely retry after receiving a 408?

Yes. RFC 9110 explicitly states that a client MAY repeat the request without modifications. The 408 means the request content was fine — it just didn't arrive in time. Use exponential backoff (1s, 2s, 4s) to avoid hammering a server that may be under load. Check for a Retry-After header in the response, which suggests how long to wait. If you get repeated 408s, the issue is likely network quality between you and the server, not the request itself.

How do I configure request timeout values on my server?

Nginx: set client_header_timeout (default 60s) for how long to wait for headers, and client_body_timeout (default 60s) for gaps between body reads. Apache: use Timeout directive (default 300s) for general I/O timeout, and RequestReadTimeout for fine-grained control over header and body reception. Node.js: server.headersTimeout (default 60000ms) and server.requestTimeout (default 300000ms). Choose values based on your expected payload sizes and client network quality — too low causes false 408s on legitimate slow clients, too high leaves you vulnerable to Slowloris attacks.