,{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is 408 retryable?","acceptedAnswer":{"@type":"Answer","text":"Yes. 408 is safe to retry. RFC 9110 notes that servers include Connection: close, so the client must open a fresh connection rather than reusing the closed one."}},{"@type":"Question","name":"What causes 408 on AWS Application Load Balancer?","acceptedAnswer":{"@type":"Answer","text":"ALB has a 60-second idle timeout by default. Keep-alive connections that sit idle longer receive a 408 or a TCP RST. Increase the ALB idle timeout in load balancer settings."}}]}

HTTP 408 Request Timeout

Quick reference

Code408
NameRequest Timeout
Category4xx Client Error
SpecRFC 9110 §15.5.9
Cacheable?No

What 408 means

The server sent a 408 Request Timeout because the client connected but did not send the complete HTTP request within the server's configured idle timeout. The server was willing to accept the request — it just never arrived in full.

RFC 9110 requires servers to include a Connection: close header with 408 responses, because the server is terminating this connection. After a 408, the connection is not reused. The client must open a new TCP connection (and TLS handshake, if applicable) to retry.

408 is generated entirely server-side. The client usually never sees the 408 status code in its user interface — browsers show connection-level errors instead. 408 is most visible in server access logs, reverse proxy logs, and API clients that inspect HTTP status codes explicitly.

How 408 is triggered

Every HTTP server maintains an idle connection timeout — the maximum time it will wait for a request to arrive on an established connection before closing it. This timeout applies to two distinct scenarios.

Keep-alive idle timeout: HTTP keep-alive (persistent connections) allows a client to reuse the same TCP connection for multiple requests. Between requests, the connection sits idle. If the client does not send the next request within the server's keep-alive timeout (commonly 5–75 seconds depending on server configuration), the server closes the connection and may emit a 408 in its log.

Slow/partial request timeout: A client opens a TCP connection and begins sending a request but sends headers or the body too slowly — either due to a slow network, a stalled upload, or a bug in the client that pauses mid-stream. The server times out waiting for the complete request and responds with 408.

A third scenario involves HTTP pipelining, where a client queues multiple requests on one connection. If the second pipelined request does not arrive before the pipeline timeout, a 408 is returned for it.

Server configuration

The server-side timeout that controls 408 behavior varies by software:

nginx — client_header_timeout (default 60s) controls how long nginx waits to read the request header. client_body_timeout (default 60s) controls body reads. A timeout in either fires a 408:

server {
    client_header_timeout 30s;
    client_body_timeout   30s;
    keepalive_timeout     65s;
}

Apache — The Timeout directive (default 300s) and RequestReadTimeout module control request reading. KeepAliveTimeout (default 5s) covers keep-alive idle time:

Timeout 60
KeepAliveTimeout 15
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500

Node.js (http module) — Use server.headersTimeout (default 60000 ms) and server.requestTimeout (default 300000 ms). When these fire, Node.js emits a 408 automatically in recent versions.

Client-side fixes

When an API client receives a 408, the response includes Connection: close. Retry logic must open a new connection — not reuse the existing one. Most HTTP libraries handle this automatically if connection reuse is enabled, but some older clients attempt to reuse the closed connection and fail.

Python requests:

import requests
from requests.adapters import HTTPAdapter

session = requests.Session()
# Disable connection pooling for endpoints known to 408:
adapter = HTTPAdapter(max_retries=3)
session.mount('https://', adapter)

response = session.get('https://api.example.com/data', timeout=(5, 30))
if response.status_code == 408:
    # Force new connection on retry
    session.close()
    response = session.get('https://api.example.com/data', timeout=(5, 30))

curl: curl opens a new connection automatically after a 408. Add --retry 3 --retry-connrefused to retry with backoff.

fetch (browser/Node.js): The Fetch API does not automatically retry on 408. Implement retry with exponential backoff:

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    const res = await fetch(url, options);
    if (res.status !== 408) return res;
    await new Promise(r => setTimeout(r, 500 * 2 ** i));
  }
  throw new Error('Max retries reached after 408');
}

408 in server logs

nginx logs 408 with the client address, time, and bytes received. A pattern of 408s from the same IP often indicates a crawler or bot holding connections open without sending requests — a common slowloris-style behavior. ModSecurity and Fail2Ban can detect and block this pattern:

# nginx log showing 408
192.0.2.55 - - [25/Apr/2026:14:05:11 +0000] "" 408 0 "-" "-"

# Fail2Ban filter for nginx 408 floods
[Definition]
failregex = ^ -.*" 408 .*$
ignoreregex =

408 vs related codes

CodeWho timed outWhat was waiting
408ServerComplete request from client
504 Gateway TimeoutProxy/gatewayResponse from upstream server
524 (Cloudflare)Cloudflare edgeResponse from origin after connection
503 Service Unavailable—Server overloaded, not a timeout

Frequently asked questions

Is 408 retryable?

Yes. 408 is safe to retry. RFC 9110 notes that servers include Connection: close, so the client must open a fresh connection rather than reusing the closed one. Retrying on a new connection is always safe for 408.

What causes 408 on AWS Application Load Balancer?

ALB has a 60-second idle timeout by default. Keep-alive connections that sit idle longer receive a 408 or a TCP RST. Increase the ALB idle timeout in the load balancer settings, or ensure clients send requests within the timeout window.

Why do I see 408 in nginx logs with an empty request line?

An empty request line ("") in nginx logs alongside a 408 means a client opened a TCP connection but never sent any bytes. This is characteristic of health checkers, bots probing for open ports, or keepalive connections that timed out without a follow-up request.

Does 408 count against rate limits or quotas?

Generally no — 408 fires before request processing completes, so it typically does not decrement API quota counters. However, this is API-implementation-specific; check the vendor's documentation if quota accuracy matters.

Related guides

HTTP 504 · HTTP 503 · HTTP 429 · Cloudflare 522 · Cloudflare 524 · Nginx 499

Client Errors Hub · All Guides · Home