Troubleshooting

408 Request Timeout error: what it means and how to fix it

A 408 error means the server stopped waiting for your request to arrive. What RFC 9110 says, visitor fixes, and nginx, Apache and Node.js timeouts.

Published ~10 min read
On this page

Short answer: 408 Request Timeout means the server gave up waiting for the client to finish sending its request. The request line and headers, or the body of an upload or a form, did not arrive within the server’s read timeout, so it closed the connection without processing anything. As a visitor, reload the page; if it keeps happening, the slow part is your connection or a VPN or proxy on the way. As a site owner, find out which server answers with the 408, then raise its body timeout for upload routes only: client_body_timeout in nginx, RequestReadTimeout in Apache, requestTimeout in Node.js.

Debian · Ubuntu · recent 408s and who caused them
# nginx: in the combined log format the status follows the quoted request,
# which is also "-" or cut short on many 408 lines, so split on the quotes
sudo awk -F'"' '$3 ~ /^ 408 /' /var/log/nginx/access.log | tail -5
sudo awk -F'"' '$3 ~ /^ 408 / {split($1, a, " "); print a[1]}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

# Apache
sudo awk -F'"' '$3 ~ /^ 408 /' /var/log/apache2/access.log | tail -5

What does 408 Request Timeout mean?

RFC 9110, the current HTTP specification, defines it in one sentence: the server “did not receive a complete request message within the time that it was prepared to wait”. It adds that a client with a request still in transit may send it again, on a new connection if the old one is no longer usable. In the usual case the request never arrived in full and the application behind the server never acted on it, so sending it again does not do the work twice.

That makes 408 the mirror image of the timeouts people usually meet. With a 408 the server is waiting on the client. With a 504 or a Cloudflare 524, the client is fine and something on the server side is too slow:

ErrorWho gave up waitingWaiting forLook at
408 Request TimeoutThe web serverThe client to finish sending the requestThe client's network, upload size, server read timeouts
504 Gateway TimeoutA proxy such as nginxThe backend (PHP-FPM, Node, gunicorn) to answerThe backend and its slow requests
Cloudflare 524CloudflareYour origin's first byte, 125 seconds by defaultThe slow request on your server
499 (nginx log only)The clientnginx's responseNever sent to anyone: nginx logs it when the client hangs up first
ERR_CONNECTION_TIMED_OUTThe browserA TCP connection at allFirewall, DNS, routing

How do I fix a 408 error as a visitor?

  • Reload once. A 408 is often a single stalled request on a busy or weak connection, and the next attempt goes through.
  • Check the upload direction of your connection. A 408 is about data you send, so it shows up on uploads: a large photo, a video, a form with attachments. Home and mobile connections often upload far slower than they download, and a backup or cloud sync running at the same time can saturate that direction completely. Pause it and try again, or switch from weak Wi-Fi or mobile data to a wired or stronger connection.
  • Try without the VPN or proxy. Every hop adds a place where a request can stall. If the page works without it, the VPN or proxy is the slow part.
  • Still failing everywhere? Then the site’s timeout is too strict for what it asks you to upload. Tell the site owner which page and what file size; only they can change it.

Does nginx send a 408 at all?

nginx’s documentation says a request that runs into client_header_timeout or client_body_timeout “is terminated with the 408 (Request Time-out) error”. What that means in practice is in nginx’s source code (ngx_http_request.c, read for this article): nginx writes 408 to the access log and closes the connection, without sending a 408 response. The visitor sees a connection error, not an error page, usually ERR_EMPTY_RESPONSE after a clean close, or ERR_CONNECTION_RESET when reset_timedout_connection is on, as described in ERR_CONNECTION_RESET.

Two consequences follow:

  • A 408 page in the browser did not come from nginx. It came from an Apache server, a Node.js or other app server facing the visitor directly, a load balancer, or an application that returns 408 itself.
  • A connection that never sent a byte leaves no access log line. nginx closes it after client_header_timeout and notes only client timed out at the info level of the error log, which the default level does not show. So a 408 line in nginx’s access log means the client did start a request and then stalled.

What a real 408 response looks like

Node.js’s built-in HTTP server does send a 408. To show it, a test server ran on Debian 12 with Node.js 25.7.0, with its timeouts shortened to 2 and 4 seconds. A raw socket sent it two incomplete requests: headers without the closing blank line, and a POST that announced 100 bytes of body and sent 10.

Example output · Node.js test server, raw socket client
--- headers never finished: closed after 2.0 s
HTTP/1.1 408 Request Timeout
Connection: close

--- body stalls (Content-Length 100, 10 bytes sent): closed after 4.0 s
HTTP/1.1 408 Request Timeout
Connection: close

The second case is the one that matters for uploads: the request handler had already started reading the body, and Node still answered 408 when the whole request was not in after requestTimeout. Apache answers with a 408 the same way once the request line has arrived and the headers or the body stall.

Reading the 408 lines in your access log

Run the commands at the top of this article and look at what the 408 lines have in common:

  • POSTs to an upload or import route: real users with slow or unstable connections. Raise the body timeout for that route, as below.
  • A request field that is just "-" or cut off: the connection was opened and the request line never completed. On Apache these are often spare connections: browsers open some in advance and do not always use them. Scanners and broken clients leave the same trace. Spread across many addresses at a low rate, they are noise. nginx logs nothing for a connection that sent nothing, so on nginx such a line means a request line that started and then stalled.
  • Many from one address at once: either a broken client or a deliberate slow-request attack (slowloris), which opens connections and sends headers a few bytes at a time to tie up the server. Short header timeouts are the defence, not longer ones; see what a DDoS attack is for connection limits per address.

Fix on nginx: client_header_timeout and client_body_timeout

DirectiveDefaultWhereWhen it runs out
client_header_timeout60shttp, serverLogs 408, closes the connection
client_body_timeout60s, between two readshttp, server, locationLogs 408, closes the connection
keepalive_timeout75shttp, server, locationCloses an idle connection quietly, no 408
send_timeout60s, between two writeshttp, server, locationCloses the connection while sending the response

client_body_timeout is not a limit on the whole upload. It counts only the time between two successive reads, so a slow but steady upload never hits it, however long it takes. It fires when the client sends nothing at all for 60 seconds, which is what a mobile connection does when it drops out for a minute. Raise it where large uploads happen and keep the header timeout short everywhere, since a real browser sends its headers in a fraction of a second:

/etc/nginx/sites-available/example.com
server {
    # ... your existing listen, server_name and ssl_* lines stay as they are ...
    client_header_timeout 15s;          # headers: short, slow-request defence

    location /upload {
        client_body_timeout 180s;       # allow a 3-minute stall mid-upload
        client_max_body_size 512m;
        proxy_pass http://127.0.0.1:3000;
    }
}
Debian · Ubuntu · find every value, then reload
sudo nginx -T 2>/dev/null | grep -nE 'client_(header|body)_timeout|keepalive_timeout'
sudo nginx -t && sudo systemctl reload nginx

client_header_timeout cannot go in a location; it belongs in the server or http block. Raising keepalive_timeout does nothing for a 408: it governs how long an idle connection stays open between requests, and when it runs out nginx closes the connection without logging anything. If the upload fails with a 413 rather than a timeout, the size limit is the problem, covered in 413 Request Entity Too Large.

Fix on Apache: RequestReadTimeout

Apache’s read timeouts come from mod_reqtimeout. Upstream’s default is handshake=0 header=20-40,MinRate=500 body=20,MinRate=500. Debian’s apache2 package ships its own file, /etc/apache2/mods-available/reqtimeout.conf, with a stricter body setting:

Debian package · reqtimeout.conf (comments removed)
RequestReadTimeout header=20-40,minrate=500
RequestReadTimeout body=10,minrate=500

Read header=20-40,minrate=500 as the file’s own comments explain it: wait up to 20 seconds for the first byte of the request, then require at least 500 bytes per second, but never wait more than 40 seconds in total. The body setting waits 10 seconds for the first byte of the body and then requires 500 bytes per second with no upper limit.

Unlike nginx, Apache sends a 408 response when the headers or the body stall. When not even the request line arrives, Apache’s source (server/protocol.c) only logs a 408 with an empty request (for the first request on a connection) and closes the connection without an answer: that is where the "-" 408 lines in an Apache access log come from. The timeout itself is logged at LogLevel info. To relax the body rule for one site with big uploads, set it in that virtual host:

Apache · inside the <VirtualHost> block
RequestReadTimeout header=20-40,MinRate=500 body=60,MinRate=200
Debian · Ubuntu · test and reload
sudo apachectl configtest && sudo systemctl reload apache2

The directive works in the server config and in virtual hosts, not in .htaccess. Do not disable mod_reqtimeout to make the errors go away: it is Apache’s built-in protection against slow-request attacks.

Node.js and apps behind a proxy

Node.js sets three timeouts on every HTTP server. On Node.js 25.7.0 the defaults read back as headersTimeout 60,000 ms, requestTimeout 300,000 ms and keepAliveTimeout 5,000 ms. requestTimeout covers the whole request, body included, so an upload that takes longer than five minutes straight to Node ends in a 408 even if bytes keep flowing.

Node.js · raise the limit for an upload service
const server = http.createServer(app);
server.requestTimeout = 30 * 60 * 1000;   // 30 minutes for the whole request
server.headersTimeout = 60 * 1000;        // headers: leave at the 60 s default

Raise it rather than set it to 0: zero switches the limit off, and a server that faces the internet directly then waits on a client that never finishes for as long as the client likes. With nginx in front, the problem usually does not reach the app at all: proxy_request_buffering is on by default, so nginx reads the entire body from the slow client first and hands it to the backend in one quick burst. Turn buffering off for streaming uploads and the backend’s own timeouts apply to the client’s speed again.

How do I keep 408s rare?

  • Keep header timeouts short and body timeouts generous on upload routes only. The first protects the server, the second protects users on bad connections.
  • Upload big files in chunks. A resumable uploader sends a large file as many small requests, so one stall costs a few seconds of retry instead of the whole upload.
  • Do not push server-sized files through a browser. Backups, database dumps and media archives belong on SFTP or rsync, which resume and do not care about HTTP timeouts; see transferring files to a VPS.
  • Rule out a full disk. nginx writes request bodies larger than client_body_buffer_size to a temporary file. On a full disk those uploads fail too, but with a 500 and No space left on device in the error log rather than a 408, so check disk usage before tuning timeouts.

All of these settings live in files you can only edit with root access. On a Linux VPS you set nginx, Apache and app server timeouts yourself, per site and per route, instead of living with whatever a shared host picked for everyone.

Frequently asked

What does a 408 error mean?

408 Request Timeout means the server did not receive the complete request, its headers or its body, within the time it was prepared to wait, so it closed the connection without processing anything. The server is the side that waited and the client or the network was too slow, the opposite of a 504.

How do I fix a 408 Request Timeout?

As a visitor, reload, pause other uploads and try a stronger connection without a VPN. As a site owner, raise the body timeout for upload routes only: client_body_timeout in nginx (60 seconds between two reads by default), the body part of RequestReadTimeout in Apache, or server.requestTimeout in Node.js. Keep header timeouts short, since they protect against slow-request attacks.

Does nginx send a 408 page?

No. When client_header_timeout or client_body_timeout runs out, nginx writes 408 to its access log and closes the connection without sending a response, so the browser shows a connection error instead. A 408 page that a visitor actually sees comes from Apache, a Node.js server, a load balancer or the application.

What is the difference between 408 and 504?

With a 408 the web server waited for the client to finish sending the request. With a 504 Gateway Timeout a proxy such as nginx waited for the backend to answer. A 408 points at the client's connection or a stalled upload, a 504 at a slow application behind the proxy.

Related articles

All troubleshooting