SiteError.comYour friendly guide to HTTP status codes
Status CodesBlog
  1. Home
  2. Blog
  3. Understanding HTTP 408 Request Timeout: Who's Actually Too Slow, and Why It Isn't 504

Understanding HTTP 408 Request Timeout: Who's Actually Too Slow, and Why It Isn't 504

September 3, 20268 min read
4xxClient Error

408 Request Timeout gets blamed for the wrong kind of slowness more often than any other timeout code. It sounds like it should mean "the server took too long," but it's the opposite: 408 is the server giving up on a client that was too slow to finish sending its request. This post covers what 408 actually promises, why it's a different animal from 504 Gateway Timeout and NGINX's 499, the connection-close behavior baked into the spec, and how to return it correctly.

What Is a 408?

A 408 tells the client that the server was waiting for the rest of a request — headers, body, or both — and gave up before it arrived.

The 408 (Request Timeout) status code indicates that the server did not receive a complete request message within the time that it was prepared to wait. A server SHOULD send the "close" connection option in the response, since 408 implies that the server has decided to close the connection rather than continue waiting. — RFC 7231, Section 6.5.7

In plain English: "You started sending me a request, and I waited as long as I was willing to, but you never finished." This is about the request arriving too slowly on the way in — a slow or flaky client connection, a request line that trickles in a few bytes at a time, or a client that opens a connection and then just sits there. Because the server has effectively decided the connection is unsalvageable, the spec expects it to close the connection along with sending the 408 — not leave it open for reuse.

Don't use 408 for application-level timeouts, like a slow database query or a backend call that's taking forever to answer. Use 408 only when the HTTP request itself wasn't fully received in time. For slow operations happening after the request was received, that's 504 Gateway Timeout or an application-level error, not 408.

Who's Actually Slow: The Direction Matters

Every "timeout" status code is really answering one question — whose timer expired, waiting on whom? — and 408 is unusual among them because it points inward rather than outward:

  • 408 is about incoming data. The server is receiving a request and the client is too slow supplying it. The server's problem is upstream of its own processing — it never even got the full instructions.
  • 504 is about outgoing dependency. A proxy or gateway is waiting on someone else — an upstream server or backend — to respond, and that wait exceeded its patience.
  • 499 Client Closed Request (NGINX's non-standard code) is the mirror image of both: the client gave up first and disconnected before the server could respond at all.

A useful way to keep them straight: 408 is "you were too slow telling me what you wanted," 504 is "I was too slow hearing back from someone else on your behalf," and 499 is "you stopped listening before I could answer." All three involve a clock running out, but the party being timed and the direction of the wait are different in each case.

Connection Reuse: Why the Server Closes the Connection

The RFC's "SHOULD send the close connection option" instruction isn't incidental — it reflects a real constraint. If a client was too slow assembling a request once, the server has no good way to know it's dealing with a well-behaved client on a slow network versus something stuck or misbehaving. Rather than leave a persistent (keep-alive) connection open and risk waiting on the same client indefinitely, the server tears the connection down and forces a fresh TCP handshake for the next attempt. That's also why MDN's documentation notes that some servers just close the connection outright without ever sending a 408 at all, since going to the trouble of composing and sending a response body to a client that's already proven unreliable has limited value. If you've ever seen a client report a bare connection reset instead of an HTTP 408, this is why — both are spec-compliant reactions to the same situation.

408 vs 504 vs 499

CodeMeaningWho timed outStandard?
408 Request TimeoutClient was too slow sending the requestThe server, waiting on the clientYes (RFC 9110)
504 Gateway TimeoutUpstream was too slow respondingA proxy/gateway, waiting on a backendYes (RFC 9110)
499 Client Closed RequestClient disconnected before a response arrivedThe client gave up on the serverNo (NGINX-specific, log-only)

Common Causes

The situations that legitimately produce a 408:

  1. The client took too long to send the request — a slow connection, a large request body trickling in below the server's minimum-rate threshold, or a client that stalls partway through.
  2. Network issues causing delays — packet loss, high latency, or an unstable connection that stretches the time needed to deliver a complete request well past what the server budgeted for.
  3. The server closing idle connections — a client opens a keep-alive connection, doesn't send a new request within the idle timeout, and the server reclaims the connection with (or without) a 408.

Returning 408 Correctly

408 is unusual among 4xx codes in that application code rarely generates it directly — it's almost always the layer terminating the TCP/HTTP connection (a reverse proxy, a load balancer, or the HTTP server itself) that decides a request took too long to arrive and closes things down. Framework-level route handlers don't typically get the chance to return a 408, because the request never finished arriving in the first place.

NGINX

NGINX has two directives that govern how long it will wait for a client to finish sending a request, and either one timing out results in a 408 being logged (and typically sent, connection permitting):

http {
    # How long to wait for the client to send the request headers
    client_header_timeout 15s;
 
    # How long to wait for the client to send the request body
    client_body_timeout 15s;
}

Lowering these values protects the server from slow-client resource exhaustion (a class of attack sometimes called "Slowloris"), at the cost of being less forgiving toward legitimately slow connections — mobile clients on weak signal, for instance.

Node.js (raw http server)

Node's built-in HTTP server does not synthesize a 408 body automatically; if you want one, you handle it via the server's own timeout:

import http from "node:http";
 
const server = http.createServer((req, res) => {
  // Normal request handling
});
 
// If the client doesn't finish sending headers within this window,
// Node emits a "timeout" event on the socket rather than a 408 response.
server.headersTimeout = 15_000; // ms
server.requestTimeout = 20_000; // ms, covers the full request
 
server.on("timeout", (socket) => {
  socket.destroy();
});

Most production Node deployments sit behind NGINX or a load balancer that terminates slow-client connections before they ever reach the application process, which is why explicit 408 handling in application code is rare.

Example response

HTTP/1.1 408 Request Timeout
Connection: close

Note the absence of a response body in the minimal case — and the Connection: close header the RFC explicitly recommends, signaling to the client (if it's still listening) that it shouldn't attempt to reuse this connection.

Common Pitfalls

  1. Returning 408 for a slow backend call instead of 504. If the request was fully received and the delay is in your code waiting on a database or an upstream API, that's not a 408 — the request arrived fine, it's the processing that's slow. Use 504 (if you're a proxy relaying an upstream timeout) or a plain 500-family error.
  2. Expecting every slow client to receive a clean 408 response. As MDN notes, some servers just drop the connection without sending anything — don't build client-side logic that assumes a 408 status line will always be there to catch.
  3. Leaving the connection open after sending a 408. The RFC's guidance to send Connection: close exists for a reason — a client that was too slow once is a bad candidate for connection reuse; don't set unusually generous keep-alive settings that undercut the timeout you just enforced.
  4. Setting client body/header timeouts so aggressively that legitimate slow clients get cut off. Mobile connections and large uploads over weak networks can legitimately take longer than a desktop broadband connection — tune client_header_timeout/client_body_timeout (or your framework's equivalent) with real traffic patterns in mind, not just worst-case abuse scenarios.

Wrapping Up

408 is a request that never finished arriving, not a response that took too long to send. The rules of thumb:

  • 408 means the client was too slow sending the request — the direction of the wait is inbound, not outbound
  • It's a different code from 504 (a proxy waiting on an upstream) and 499 (a client that gave up on the server) — all three are timeouts, but the party being timed differs
  • The RFC expects the connection to close after a 408 — don't try to keep it alive for reuse
  • Some servers skip the 408 response entirely and just close the connection, which is spec-compliant

For more, see our pages on 408 Request Timeout, 504 Gateway Timeout, and our 499 Client Closed Request and 504 Gateway Timeout posts for the rest of the timeout family.

Full Reference

408 Request Timeout

The server would like to shut down this unused connection.

Related Status Codes

🤨400Bad Request🔐401Unauthorized💳402Payment Required🚫403Forbidden🔍404Not Found🙅405Method Not Allowed🍽️406Not Acceptable🎫407Proxy Authentication Required⏰408Request Timeout⚔️409Conflict👻410Gone📏411Length Required❌412Precondition Failed📦413Payload Too Large📜414URI Too Long📼415Unsupported Media Type📖416Range Not Satisfiable😞417Expectation Failed🫖418I'm a Teapot🚪421Misdirected Request🤔422Unprocessable Entity🔒423Locked🎯424Failed Dependency⏰425Too Early⬆️426Upgrade Required🔑428Precondition Required🚦429Too Many Requests📋431Request Header Fields Too Large⚖️451Unavailable For Legal Reasons
Back to Blog

Popular Status Codes

  • 200 OK
  • 301 Moved Permanently
  • 302 Found
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable

Compare Codes

  • 401 vs 403
  • 301 vs 302
  • 404 vs 410
  • 500 vs 502
  • Compare any codes →

Categories

  • Informational
  • Success
  • Redirection
  • Client Error
  • Server Error
  • NGINX
  • Cloudflare
  • AWS ELB
  • Microsoft IIS

Tools

  • Cheat Sheet
  • Status Code Quiz
  • URL Checker
  • API Playground
  • Blog

© 2026 SiteError.com. All rights reserved.