1. Why timeouts are needed and where they apply
When you write a server for the first time, it seems like requests arrive quickly, clients behave reasonably, the internet is fair, and the cat won’t knock over the router. Reality likes a different genre: “the client sends headers one byte per second,” “the client reads the response so slowly as if it had a 1998 modem,” “the connection is kept open just because it can be.” Server timeouts are a way to say: “either we work, or we part ways—no drama.”
There’s another important reason: resources are limited. Every open socket, every stuck connection, and every “hanging response write” is memory, file descriptors, and time. When there are many such clients, the server starts to suffer even if your business logic is perfect. That’s why timeouts aren’t an “optimization,” but basic hygiene for a live service.
By the way, context.Context and deadlines as an idea exist precisely so requests can be stopped without accumulating “tails” and chewing up process resources. If a request can’t be stopped, it can create a queue and consume memory.
Where exactly these timeouts work
It’s important not just to “know the names” of timeouts, but to understand which part of a connection’s lifecycle they limit. Then you’ll stop confusing ReadHeaderTimeout and ReadTimeout, and you won’t set IdleTimeout “at random” like an oven temperature.
A simplified lifecycle of an HTTP connection can be represented like this:
flowchart TD
A[Client connected] --> B[Sends headers]
B --> C["Sends body (not always)"]
C --> D[Server processes]
D --> E[Server writes response]
E --> F[Connection idles (keep-alive)]
On top of that, the following limits apply:
- ReadHeaderTimeout limits segment B (headers).
- ReadTimeout limits B + C (the entire incoming request, including the body).
- WriteTimeout limits segment E (writing the response to the client).
- IdleTimeout limits segment F (keep-alive connection idling).
To reinforce it—a table:
| Field on http.Server | What it limits | Why it matters |
|---|---|---|
|
time allowed to read request headers | protection against “slow headers” and connection holding |
|
time allowed to read the entire request (headers + body) | protection against “slow body” and endless uploads |
|
time allowed to write the response to the client | protection against clients that read the response too slowly |
|
how long to keep a keep-alive connection without activity | so you don’t hold thousands of “empty” connections |
2. Core http.Server timeouts
ReadHeaderTimeout: protection from slow headers
When a client starts a request, it first sends headers: method, path, Host, Content-Type, possibly Authorization, and so on. If headers arrive slowly, the server is forced to hold the connection and wait, even though real work hasn’t started yet. This is a typical slowloris-class attack/abuse (the name sounds like a cute animal, but the only cute part is the name).
ReadHeaderTimeout is a way to say: “I’ll give you N seconds for headers; if you don’t make it—goodbye.” Headers are usually small and should arrive quickly, so this timeout is often set relatively low.
Example: define timeouts as constants so we don’t smear “magic numbers” across the code:
package main
import "time"
const (
readHeaderTimeout = 5 * time.Second
)
Now apply them when creating http.Server:
package main
import (
"net/http"
"time"
)
func newServer(mux http.Handler) *http.Server {
return &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
}
Here’s the key idea: ReadHeaderTimeout is not about your business logic. It’s a “security guard at the entrance” who doesn’t let into the building those who linger too long at the door.
ReadTimeout: a limit on reading the entire request
After headers, a request may have a body. For example, in a task app this could be POST /api/v1/tasks with JSON. If the client sends the body slowly, the server will again hold the connection and wait before it even truly reaches the handler.
ReadTimeout is a general timeout for the entire incoming request. It’s convenient to think of it as “the maximum acceptable time for the client to send us everything it wants to send.”
A practical nuance: the more body you allow (for example, file uploads), the more “generous” a ReadTimeout the scenario requires. But for a JSON API, where the body is usually small, you can keep ReadTimeout moderate.
An example of a typical configuration for a small JSON API:
package main
import (
"net/http"
"time"
)
func newServer(mux http.Handler) *http.Server {
return &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
}
}
Notice: even if your handler later takes a long time to think, ReadTimeout is not about “thinking.” It’s about “receiving input data.” That’s why it applies before the server confidently enters your business logic.
One more important point: ReadTimeout does not replace the idea of limiting the request body size (for example, via MaxBytesReader). Here we’re just keeping the focus on server timeouts as time boundaries, not size limits.
WriteTimeout: protection from slow response reading
Now imagine the reverse problem: the server computed everything, formed the response… and starts writing it to the network. But the client reads the response slowly. Or the network is bad. Or the client “hung,” but doesn’t close the connection. If the server tries to write forever, it again holds a resource (the connection), and you can end up with many such slow clients.
WriteTimeout is the maximum amount of time the server is willing to spend writing a response. Beginners are often surprised: “But I’m just doing json.NewEncoder(w).Encode(...)—can that really hang?” It can. Writing to the network is I/O, and I/O loves surprises.
A mini example of a handler that runs for a long time (artificially) to feel the meaning of the timeout:
package main
import (
"net/http"
"time"
)
func slowHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Second)
_, _ = w.Write([]byte("ok\n"))
}
If WriteTimeout is less than 20 seconds, writing the response may be interrupted. This doesn’t mean WriteTimeout “kills” your business logic immediately at Sleep. It rather guarantees: “when it comes time to write, writing has a limit.”
A practical takeaway: choose WriteTimeout based on how long your requests can take normally. If you have endpoints that honestly take 30 seconds (for example, heavy report generation), a 5-second WriteTimeout will break a normal scenario.
IdleTimeout: how long to keep keep-alive without activity
HTTP (especially HTTP/1.1) likes to reuse connections: the client makes a request, gets a response, but the connection doesn’t close and remains idle, waiting for the next request. This is useful: less overhead for establishing a TCP connection. But there’s a downside: if you keep idle connections forever, the server can accumulate a huge “collection of silent clients.”
IdleTimeout is the time the server is willing to keep a connection open without activity, waiting for the next request. This is not about reading/writing a particular request, but about pauses between requests.
A typical configuration:
package main
import (
"net/http"
"time"
)
func newServer(mux http.Handler) *http.Server {
return &http.Server{
Addr: ":8080",
Handler: mux,
IdleTimeout: 60 * time.Second,
}
}
Why do people often set around a minute (or a few minutes)? Because it’s a compromise. Too small an IdleTimeout will make life worse for normal clients (they’ll have to reconnect more often). Too large will allow holding many “empty” connections.
3. Collect server settings in one place
Now we’ll do exactly what a Go developer should be able to do: turn a set of scattered fields into a clear, maintainable configuration. We won’t scatter 5*time.Second all over main.go. We’ll move the timeout policy into one place and assemble http.Server neatly.
First, declare constants (or package-level variables, but constants read simpler here):
package main
import "time"
const (
readHeaderTimeout = 5 * time.Second
readTimeout = 10 * time.Second
writeTimeout = 15 * time.Second
idleTimeout = 60 * time.Second
)
Now create the server with a single function:
package main
import "net/http"
func newServer(addr string, mux http.Handler) *http.Server {
return &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
}
Notice a “small thing” that actually makes the code more mature: addr is passed as a parameter. Even if for now you always use ":8080", this style makes life easier when you want to run a test server on a different port.
Next, in main you hook up the mux and start the server. Here is only the central part:
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok\n"))
})
srv := newServer(":8080", mux)
log.Printf("listening on %s", srv.Addr)
log.Fatal(srv.ListenAndServe())
}
Yes, the graceful shutdown scaffolding is usually a bit more complex (that’s where signal.NotifyContext, Shutdown, and filtering of http.ErrServerClosed appear), but the important thing here is different: timeouts are set when creating http.Server and they work during the normal life of the server.
4. How to choose timeout values
You want an honest recipe: “set 3 seconds here, 7 there, and live happily.” But the values depend on the nature of requests, the network, the clients, and what you fear more: “hangs” or “false disconnects.”
So the practical strategy is: first pick reasonable defaults for a small JSON API, then observe behavior in environments close to production, and only then adjust.
For small requests, headers almost always arrive quickly, so ReadHeaderTimeout is usually made small. For reading a JSON body, a few seconds or a dozen seconds is often enough. For writing a response, WriteTimeout is often a bit larger, because writing depends on the client. For IdleTimeout, tens of seconds or minutes are often chosen so keep-alive works, but no one lives “forever.”
A useful principle: timeouts should read like policy. When someone opens your code, they should immediately see: “the server waits 5 seconds for headers, 10 seconds for the full request, writes the response for up to 15 seconds, keeps keep-alive for a minute.” If instead they see “13, 17, 19” scattered across the project, that’s not configuration—it’s a quest.
5. Common mistakes
Mistake #1: leaving all timeouts at zero “because it works anyway.”
A zero value for most timeouts means “no limits.” In tests it looks fine: everything is fast, all clients are good. In reality you get a server that can hold connections forever, and the problem won’t show up as a nice bug, but as “why did everything die on Friday evening.”
Mistake #2: confusing shutdown timeout with server timeouts.
The timeout in Server.Shutdown(ctx) limits how long you’re willing to wait for a graceful shutdown when stopping the process. ReadTimeout/WriteTimeout/IdleTimeout work during the server’s normal life. Trying to replace one with the other leads to strange effects: you “set a timeout,” but the server keeps hanging on slow clients.
Mistake #3: setting WriteTimeout too small and then being surprised by “disconnects.”
If your handler sometimes honestly runs for 10–20 seconds, but WriteTimeout is set to 3 seconds, the client will get truncated responses. This is especially unpleasant because you start looking for a bug in JSON, in the codec, in business logic—when the culprit is simply a too-strict timeout.
Mistake #4: setting IdleTimeout huge so clients “reconnect less often,” and getting a graveyard of idle connections.
Keep-alive is useful, but it shouldn’t turn into an endless TCP-level “chat.” If IdleTimeout is very large, the server holds many connections for a long time that nobody needs anymore. This is a quiet resource leak: slow, boring, and therefore especially dangerous.
Mistake #5: smearing numbers through the code and losing control over the time policy.
When one file has ReadTimeout: 7*time.Second, another has WriteTimeout: 12*time.Second, and somewhere else IdleTimeout: time.Minute + 15*time.Second, the configuration turns into a set of accidents. A month later you no longer remember why the numbers are like that. It’s much easier to keep timeouts together, in constants, so the policy is visible immediately and changes are made intentionally.
GO TO FULL VERSION