In my current project team faced a strange issue. Some requests to our service failed without leaving any traces in logs and APM. When we dig into it, the first clue that we found was higher data size these requests process that requires more time to execute the request. It ended up to be standard behavior for not empty
http.Server.WriteTimeout parameter. This article describes
http.Server.WriteTimeout behavior and compares it with an alternative solution -
TimeoutHandler are both designed to solve the same problem. They limit request execution time in HTTP Server to protect resources on client and server. Without limiting, open connections live forever slowing down the application and eventually making it not reachable.
Handler code interruption Both these utilities controls the max time client waits for a response. On another side, server execution is not interrupted when a timeout is reached.
TimeoutHandler have different internal logic, but they both do not terminate the execution of the handler goroutine. You can expect that long-running work inside the handler can be finished even though the request is terminated for the client.
WriteTimeout defined in
http.Server and its value applied to all handlers with not Hijacked connections. The
TimeoutHandler function is more flexible. It implements a Handler Middleware pattern. A developer can apply it to all handlers if it wraps
http.Server.Handler or it could wrap individual handlers using different timeout values.
Visibility for clients
WriteTimeout simply closes TCP connection if response writing exceeds timeout. If you use
curl utility, for example, an interrupted connection looks like
curl: (52) Empty reply from server error.
TimeoutHandler does not close the connection. Instead, it keeps a connection open but returns an HTTP response with 503 Service Unavailable status. A developer can specify the body of that response as a static string when
TimeoutHandler is declared.
WriteTimeout is synchronous. Its value checked at the end of
readRequest() function. If the timeout is not equal to 0, the server sets a writing deadline on the network file descriptor. This implementation has an interesting side effect. The connection deadline is evaluated only at the moment when someone writes into that connection. It means that
WriteTimeout does exactly what its name means, but not what the developer may expect from it. The long operation to prepare the data can take longer than
WriteTimeout but the connection will be closed only when we start writing the first response byte to the connection.
TimeoutHandler is asynchronous. It starts a new goroutine that does two things when it reaches timeout. At first, it finishes the request by sending 503 Service Unavailable response to the caller. Secondly, it cancels request Context providing a callback to the handler that the developer can use to react in timeout event inside the handler. Due to its asynchronous nature,
TimeoutHandler always terminates requests on-time even if the handler executes a blocking long-running task.
Timeout mitigation in handler
WriteTimeout hides all timeout mechanics from handler code. There is no easy way to catch timeout events in server code. The handler receives no Context cancellation,
w.Write() returns no error and even returns the number of bytes greater than zero.
TimeoutHandler from the other side allows catching timeout events. First of all, the handler can listen to request Context cancellation. For example, we can rollback transactions or delete temporary files inside that callback function. Secondly,
w.Write() returns an
ErrHandlerTimeout error if we try to write to the response after the timeout event.
Go was initially shipped with
http.Server.WriteTimeout to put limits on request writing time. That parameter controls low-level connection deadline and does what is intended to do just fine.
http.TimeoutHandler was added later in Go v1.8 after Go received
http.TimeoutHandler provides better SLA, development interface, and flexibility. It is a better fit for many Go applications.