26.6 Server Timeouts: ReadTimeout, WriteTimeout, IdleTimeout
Right, let’s talk about timeouts. This isn’t just some box-ticking exercise for your app’s YAML config; this is your first and last line of defense against the chaotic, resource-hungry abyss of the public internet. A server without timeouts is like a hotel with no checkout time—eventually, you’re going to run out of rooms because a bunch of guests decided to live in the lobby, doing nothing. Let’s not run that hotel.
Go’s net/http package gives you three primary levers to pull in your http.Server struct: ReadTimeout, WriteTimeout, and IdleTimeout. They are brilliantly powerful and, frankly, a common source of confusion because their behavior is subtly interconnected. I’ll clear it up.
The Three Timeout Musketeers
Here’s the basic setup. You don’t just set these and hope for the best; you need to know what they do.
server := &http.Server{
Addr: ":8080",
Handler: myMux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
ReadTimeout: This is the maximum duration for reading the entire request, including the body. It starts ticking the moment the connection is accepted and resets every time any new data is received. A slow client sending a request byte-by-byte? This stops them. It covers the time from the first byte to the end of the headers and body (if any).
WriteTimeout: This is the maximum duration before timing out the response. It starts after the request has been fully read and resets every time you send a response byte (a header, a chunk of the body). A client with a terrible download speed that’s taking ages to receive your beautifully generated JSON? This cuts them off. Crucially, it also applies while your handler is running if you’re using streaming responses or long-polling. More on that pitfall later.
IdleTimeout: This is my favorite. It’s the maximum amount of time to wait for the next request when keep-alives are enabled. If a connection is established but just sitting there idle, not doing anything, this timer will close it after the specified duration. This is how you prevent those “lobby loiterers” from tying up connections indefinitely.
The Gotcha: WriteTimeout and Long-Running Handlers
Here’s the first thing that trips up everyone. Because of how the Go internals work, the WriteTimeout timer includes the time your handler spends running if the connection is in a state where it could write.
Let’s say you have a handler that takes 30 seconds to process something before it writes a response.
func slowHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(30 * time.Second) // Simulate brutal work
fmt.Fprintln(w, "Finally done!")
}
If your WriteTimeout is set to 10 seconds, this handler will always be killed before it can respond. The timeout starts after the read is done and includes your handler’s execution time. The designers made a choice here: the server can’t read anymore, but it’s waiting to write, so the write timeout applies.
The fix? You need to implement your own granular timeouts within the handler context for long-running processes. The WriteTimeout is a blunt instrument for the entire response lifecycle.
func betterSlowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resultCh := make(chan string, 1)
go func() {
// Do the slow thing in a goroutine
time.Sleep(30 * time.Second)
resultCh <- "Finally done!"
}()
select {
case result := <-resultCh:
fmt.Fprintln(w, result)
case <-ctx.Done():
// Client gave up (e.g., closed connection)
http.Error(w, "Operation timed out", http.StatusServiceUnavailable)
return
case <-time.After(25 * time.Second):
// Our own, more appropriate, business logic timeout
http.Error(w, "Service took too long", http.StatusServiceUnavailable)
return
}
}
Why You Must Set All Three
You need a strategy that covers all phases of a connection’s life.
ReadTimeoutprotects you from slowloris attacks (clients that trickle data slowly) and clients that never finish sending a request.WriteTimeoutprotects you from clients that are slow to read responses, preventing your resources from being stuck in aTIME_WAITstate.IdleTimeoutis essential for managing connections when using HTTP/Keep-Alive. It allows you to have a short, aggressiveReadTimeoutfor active requests while still allowing a pool of connections to stay open for subsequent requests, but not forever. This is a best practice for any server expecting multiple requests from the same client.
The default for all of these is zero, which means no timeout. This is practically never what you want in production. Set them. Your future self, debugging a mysterious memory leak, will thank you. Choose values that make sense for your application’s latency requirements—often something like 5s read, 30s write, and 2 minutes idle is a sane starting point for many APIs. Adjust from there.