
Go 1.27 Automatically Drains Unread HTTP/1 Response Bodies for Faster Connection Reuse
Go 1.27 improves HTTP/1 connection reuse by automatically draining unread response bodies. A benchmark against index.golang.org shows the same program running almost five times faster than on Go 1.26.
Unread Response Bodies Broke HTTP/1 Connection Reuse
Go's net/http package supports HTTP/1.0, HTTP/1.1, and HTTP/2, all running over TCP and usually over TLS when talking to HTTPS endpoints. Establishing a new connection is expensive: a TCP handshake takes 1 round-trip time (RTT), a TLS 1.2 handshake takes 2 RTT, and only after a total of 3 RTT can the first HTTP request be sent. In cross-region communications, one-way latencies often reach or exceed 50 ms, so connection setup can cost far more than serving a small response.
HTTP clients and servers therefore try to reuse existing connections. In HTTP/1.1, connections are persistent by default, but responses are sent back-to-back on the same byte stream. This means net/http can only find the start of the next response after the current response body has been read to the very end, as determined by the Content-Length or Transfer-Encoding header.
The Go blog post by Nicholas Husin and Damien Neil highlights a common pitfall: a function that calls http.Get, defers resp.Body.Close, and returns only the status code without ever reading the body. Through Go 1.26, when Close is called with an unread body remaining, net/http abandons the connection and opens a new one for the next request, repeatedly paying the setup cost. The issue also affects programs that read the body but stop just short of the end. For example, encoding/json.Decoder stops reading as soon as it sees the closing brace of a JSON object, so a server that terminates the response with a newline leaves that byte unread and the connection becomes unreusable. HTTP/2 does not have this problem, because it multiplexes frames over a single connection instead of sending responses strictly back-to-back.
Go 1.27 Drains Bodies Automatically
Starting in Go 1.27, net/http.Transport automatically handles this. It asynchronously drains an unread HTTP/1 response body for up to 50 ms or 256 KB, whichever limit is reached first. If the drain finishes before the next request is sent, the connection is reused; otherwise a new connection is created and the old one is placed in a pool of idle connections, potentially being reused for a future request. The implementation also skips draining entirely when a response carries the Connection: close header, since draining would be wasted work in that case.
The authors note that this behavior might evolve in future versions of Go as other possible enhancements become apparent.
Benchmark Results and Recommendation
The improvement is substantial. A test program that calls a simple status-check function 100 times against index.golang.org, a worldwide distributed site supporting TLS 1.3, takes about 710 milliseconds on Go 1.26 but only about 150 milliseconds on Go 1.27, almost five times faster.
With the new behavior, the Go team recommends that developers simply close their HTTP response bodies once they are no longer needed, using the familiar defer resp.Body.Close() pattern. The manual workarounds that many developers built over the years, such as draining bodies with io.Copy, capping the drained bytes, or racing the drain against a timeout, are no longer necessary. The authors frame this as part of Go's philosophy of simplicity: let the code stay clean and simple, and let net/http deal with the complexities on your behalf.
Sources: Hacker News
SiTech — AI-powered web development
We build fast, modern websites and bring AI into real business workflows. Have a project or a question? We'd love to help.