Connection timeouts
Let’s take a moment to improve the resiliency of our server by adding some timeout settings, like so:
package main ... func main() { ... tlsConfig := &tls.Config{ CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, } srv := &http.Server{ Addr: *addr, Handler: app.routes(), ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), TLSConfig: tlsConfig, // Add Idle, Read and Write timeouts to the server. IdleTimeout: time.Minute, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } logger.Info("starting server", "addr", srv.Addr) err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem") logger.Error(err.Error()) os.Exit(1) } ...
All three of these timeouts — IdleTimeout
, ReadTimeout
and WriteTimeout
— are server-wide settings which act on the underlying connection and apply to all requests irrespective of their handler or URL.
The IdleTimeout setting
By default, Go enables keep-alives on all accepted connections. This helps reduce latency (especially for HTTPS connections) because a client can reuse the same connection for multiple requests without having to repeat the TLS handshake.
By default, keep-alive connections will be automatically closed after a couple of minutes (the exact time depends on your operating system). This helps to clear-up connections where the user has disappeared unexpectedly — e.g. due to a power cut on the client’s end.
There is no way to increase this default (unless you roll your own net.Listener
), but you can reduce it via the IdleTimeout
setting. In our case, we’ve set IdleTimeout
to 1 minute, which means that all keep-alive connections will be automatically closed after 1 minute of inactivity.
The ReadTimeout setting
In our code we’ve also set the ReadTimeout
setting to 5 seconds. This means that if the request headers or body are still being read 5 seconds after the request is first accepted, then Go will close the underlying connection. Because this is a ‘hard’ closure on the connection, the user won’t receive any HTTP(S) response.
Setting a short ReadTimeout
period helps to mitigate the risk from slow-client attacks — such as Slowloris — which could otherwise keep a connection open indefinitely by sending partial, incomplete, HTTP(S) requests.
The WriteTimeout setting
The WriteTimeout
setting will close the underlying connection if our server attempts to write to the connection after a given period (in our code, 10 seconds). But this behaves slightly differently depending on the protocol being used.
For HTTP connections, if some data is written to the connection more than 10 seconds after the read of the request header finished, Go will close the underlying connection instead of writing the data.
For HTTPS connections, if some data is written to the connection more than 10 seconds after the request is first accepted, Go will close the underlying connection instead of writing the data. This means that if you’re using HTTPS (like we are) it’s sensible to set
WriteTimeout
to a value greater thanReadTimeout
.
It’s important to bear in mind that writes made by a handler are buffered and written to the connection as one when the handler returns. Therefore, the idea of WriteTimeout
is generally not to prevent long-running handlers, but to prevent the data that the handler returns from taking too long to write.
Additional information
The ReadHeaderTimeout setting
http.Server
also provides a ReadHeaderTimeout
setting, which we haven’t used in our application. This works in a similar way to ReadTimeout
, except that it applies to the read of the HTTP(S) headers only. So, if you set ReadHeaderTimeout
to 3 seconds, a connection will be closed if the request headers are still being read 3 seconds after the request is accepted. However, reading of the request body can still take place after 3 seconds has passed, without the connection being closed.
This can be useful if you want to apply a server-wide limit to reading request headers, but want to implement different timeouts on different routes when it comes to reading the request body (possibly using the http.TimeoutHandler()
middleware).
For our Snippetbox web application we don’t have any actions that warrant per-route read timeouts — reading the request headers and bodies for all our routes should be comfortably completed in 5 seconds, so we’ll stick to using ReadTimeout
.
The MaxHeaderBytes setting
http.Server
includes a MaxHeaderBytes
field, which you can use to control the maximum number of bytes the server will read when parsing request headers. By default, Go allows a maximum header length of 1MB.
If you want to limit the maximum header length to 0.5MB, for example, you would write:
srv := &http.Server{ Addr: *addr, MaxHeaderBytes: 524288, ... }
If MaxHeaderBytes
is exceeded, then the user will automatically be sent a 431 Request Header Fields Too Large
response.
There’s a gotcha to point out here: Go always adds an additional 4096 bytes of headroom to the figure you set. If you need MaxHeaderBytes
to be a precise or very low number you’ll need to factor this in.