# 408 Request Timeout

> 408 Request Timeout means the server gave up waiting for the client to finish sending. Tune nginx, Apache, Node and ALB timeouts and tell it apart from 504.

Source: https://howhttpworks.com/status-codes/408
Last reviewed: 2026-10-05

> **TL;DR:** 408 Request Timeout means the server waited too long for your client to finish sending the request. Check upload stalls and request-read timeouts, then retry on a fresh connection when the operation is safe to repeat.

RFC 9110 section 15.5.9: the server "did not receive a complete request message within the time that it was prepared to wait". It usually sends `Connection: close`, and a client "MAY repeat that request on a new connection". Some servers send 408 on an unused connection, including connections opened by browser preconnect. Match the log entry to an actual failed request before treating it as an upload failure. [MDN describes unused connections](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408).

An illustrative HTTP/1.1 response:

```http
HTTP/1.1 408 Request Timeout
Connection: close
Content-Length: 0
```

## What you see in your client

These are the client formats for a response carrying 408 and the reason phrase `Request Timeout`; the URLs are placeholders. They are derived from library source, rather than captured server output. A server-supplied reason phrase can change the Python, .NET and Spring text.

- **Axios:** `AxiosError: Request failed with status code 408`. With the default status validation, the promise rejects; inspect `error.response.status`, `error.response.data` and `error.response.headers`. [Axios constructs this message in `settle`](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **fetch:** the promise resolves when the HTTP response arrives. `response.status === 408` and `response.ok === false`; check those before reading a success payload. A network failure rejects separately. [MDN documents this distinction](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).
- **Python requests:** `requests.exceptions.HTTPError: 408 Client Error: Request Timeout for url: https://api.example.com/resource`. This comes from `response.raise_for_status()`, not from `requests.get()` alone. Save the body before raising if it contains useful diagnostics. [Requests source](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **curl `-f`:** `curl: (22) The requested URL returned error: 408`. `curl -i` shows headers and the error body without fail mode; `--fail-with-body` keeps the body while returning a failing exit code. [curl source](https://github.com/curl/curl/blob/master/lib/http.c), [option documentation](https://curl.se/docs/manpage.html#--fail-with-body).
- **.NET:** `System.Net.Http.HttpRequestException: Response status code does not indicate success: 408 (Request Timeout).` This is the English message from `EnsureSuccessStatusCode()`; inspect the `HttpResponseMessage` first when you need its body. [Runtime message template](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring:** `WebClientResponseException` with message `408 Request Timeout from GET https://api.example.com/resource` for `WebClient.retrieve()`. RestTemplate's default error handler uses `HttpClientErrorException`; its message can include the response body. These class names follow the [Spring 6.2 WebClient source](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java) and [HTTP client source](https://github.com/spring-projects/spring-framework/blob/v6.2.0/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java).

## Who sent it?

| Evidence | Layer |
| --- | --- |
| nginx log `client timed out (110: Connection timed out) while reading client request headers` or `... request body` | nginx `client_header_timeout` / `client_body_timeout` |
| Apache body `Request Timeout - Server timeout waiting for the HTTP request from the client.`; log `AH01382: Request header read timeout` or `AH01382: Request body read timeout` | `mod_reqtimeout` |
| ALB access log `elb_status_code` 408 and target status `-` | ALB: client did not send a complete request within the idle timeout (60 s default) |
| Node returns it with no handler involvement | `http.Server` `requestTimeout` (300 s default since Node 18) or `headersTimeout` (minimum of 60 s and `requestTimeout`) |
| Empty/unfinished request line and no user error | A connection opened without a complete request; correlate with client activity |

## Fix it, most common first

1. **Slow or large uploads on a poor link.** Raise the body timeout or upload in chunks (resumable uploads, S3 multipart, tus). Reducing the payload (compression, image resize) is better than a long timeout, which also leaves connections open longer under slow-client attacks.
2. **Client opens a connection and sends nothing.** Browser preconnect and speculative connections cause 408s in access logs for requests nobody made. Correlate them with an empty request line and client activity before filtering alerts.
3. **Keep-alive races behind ALB.** Match the log status first. A target closing its connection before ALB expects it can cause [502](https://howhttpworks.com/status-codes/502). Set the target keep-alive timeout above the ALB idle timeout to address that separate failure, rather than treating every closed socket as 408.
4. **Proxy buffering a slow upload.** nginx reads the full body before proxying by default; a slow client trips `client_body_timeout`, not your app's timeout.
5. **Client-side tools.** Postman and curl have own timeouts (`curl --max-time`, `--connect-timeout`) that report errors differently, as `curl: (28)`, not a 408.

## Common causes by stack

- **nginx:** an incomplete header or stalled body exceeds the client-read timeout; inspect the `client timed out` error-log context and the directives below. Both client-read defaults are 60 seconds. [nginx core module](https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_timeout).
- **Apache:** `mod_reqtimeout` expires its header/body read budget or minimum data rate. Check `AH01382`, then the `RequestReadTimeout` rule. [Apache source](https://github.com/apache/httpd/blob/2.4.x/modules/filters/mod_reqtimeout.c).
- **Express on Node.js:** the HTTP server parser can emit 408 before Express receives the request. Inspect `server.headersTimeout` and `server.requestTimeout` rather than a route handler or database query timeout. [Node HTTP timeouts](https://nodejs.org/api/http.html#serverrequesttimeout).
- **AWS ALB:** the client sends no data within the configured idle interval. TCP keep-alives do not reset it; AWS requires at least one byte of data within each interval. Check `elb_status_code` against `target_status_code` in the access log. [AWS troubleshooting](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html#http-408-issues).

## Fix by stack

### nginx

```nginx
http {
    client_header_timeout 15s;   # default 60s
    client_body_timeout   120s;  # default 60s; time between two successive reads, not total
    send_timeout          60s;
    keepalive_timeout     75s;
}
```

A header-read timeout closes the nginx request with status 408; the client may receive a closed connection rather than a response body. Match the access log with the error-log context. [nginx request handling](https://github.com/nginx/nginx/blob/master/src/http/ngx_http_request.c).

### Apache

```apache
LoadModule reqtimeout_module modules/mod_reqtimeout.so
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500
```

### Node.js and Go

```javascript
const server = http.createServer(app)
server.keepAliveTimeout = 65_000   // above ALB idle timeout (60 s)
server.headersTimeout   = 66_000   // chosen header-read budget; independent of idle keep-alive
server.requestTimeout   = 120_000  // whole request, 408 if exceeded; 0 disables
```

Go uses [socket read deadlines](https://pkg.go.dev/net/http#Server); configure their header, whole-request and idle budgets separately:

```go
srv := &http.Server{
    ReadHeaderTimeout: 10 * time.Second,
    ReadTimeout:       60 * time.Second,
    IdleTimeout:       75 * time.Second,
}
```

### AWS ALB

Raise the load balancer attribute `idle_timeout.timeout_seconds` (default 60) only if clients really send slowly; otherwise fix the client. ALB 408s count under `HTTPCode_ELB_4XX_Count`.

## Client handling

Treat 408 like a connection error: retry the request on a new connection with backoff. It is safe only when the method is idempotent or the server never began processing; for POST add an `Idempotency-Key` where the API supports one. Cap attempts and keep a record of the final status.

## Reproduce and verify

```bash
# Send a complete header and stall the body: use a local test server
(printf 'POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: 100\r\n\r\npartial'; sleep 90) \
  | nc localhost 8080

# A continuous trickle tests total request time, not nginx gaps between reads
curl -v --limit-rate 100 -T bigfile.bin https://example.com/upload
```

## 408 vs 504 and 524

- 408: the client was too slow sending the request.
- [504 Gateway Timeout](https://howhttpworks.com/status-codes/504): a proxy waited too long for the upstream response.
- [524](https://howhttpworks.com/status-codes/524): Cloudflare connected to the origin but received no response within its proxy read timeout (125 seconds by default).
- [429](https://howhttpworks.com/status-codes/429): the client sent too many requests, not too slowly.
- [400](https://howhttpworks.com/status-codes/400): the request arrived but was invalid.

Try timeouts hands-on in the [request builder](https://howhttpworks.com/tools/playground).
