If your Go service calls an LLM API, a webhook receiver or any REST endpoint over HTTP/1, there is a good chance it has been opening a fresh TCP and TLS connection for almost every request. The cause is small: a response body you closed without reading to the end. On October 14 the Go team (Nicholas Husin and Damien Neil) wrote up why this happens and what Go 1.27 changes. In their benchmark, 100 calls to index.golang.org over HTTP/1 took about 710 ms on Go 1.26 and about 150 ms on Go 1.27.
This post gives you a program that shows the problem on your own machine, the two patterns that cause it, and what to do if you can't move to Go 1.27 yet.
Why an unread body breaks reuse
HTTP/1 sends responses back to back on the same connection. The client can only find where the next response starts once it has read the current body to the very end. If you call resp.Body.Close() while bytes are still unread, older versions of net/http can't safely reuse the connection, so they close it. Your next request pays for a new TCP handshake and, on HTTPS, a new TLS handshake.

The Go blog names two common ways this happens:
- Ignoring the body. You only care about the status code, but the server still sends something like
{"success": true, "message": "update processed"}. - Stopping just short of the end.
json.NewDecoder(resp.Body).Decode(&v)stops after the closing}. A trailing newline after the JSON object is still unread, and that is enough.
The first pattern looks completely innocent:
func getStatus(url string) (int, error) {
resp, err := http.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}What Go 1.27 changes
From Go 1.27, net/http.Transport drains an unread HTTP/1 response body itself when you close it. Per the Go blog, the drain:

- runs asynchronously in the background, so
Close()doesn't block; - gives up after 50 ms or 256 KB, whichever comes first, so a huge or slow body doesn't tie up the connection;
- is skipped when the response carries
Connection: close, since that connection can't be reused anyway.
So getStatus above now reuses connections with no code change. This is the result of their 100-request benchmark:
Roughly 5x, and all of it comes from not redoing handshakes. The further away the server is, the bigger the gap: every new connection costs at least one extra round trip for TCP plus more for TLS.
See it on your machine
This program makes sequential GET requests and counts how many went out on a reused connection, using httptrace.GotConnInfo.Reused. With no -url it starts a local test server that returns a small JSON body with a trailing newline. -drain reads the body to EOF before closing.
package main
import (
"flag"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"time"
)
func main() {
url := flag.String("url", "", "URL to call (default: a local test server)")
n := flag.Int("n", 100, "number of sequential requests")
drain := flag.Bool("drain", false, "read the body to EOF before closing")
flag.Parse()
if *url == "" {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"success": true, "message": "update processed"}`)
}))
defer srv.Close()
*url = srv.URL
}
reused := 0
trace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
if info.Reused {
reused++
}
},
}
start := time.Now()
for i := 0; i < *n; i++ {
req, err := http.NewRequest("GET", *url, nil)
if err != nil {
panic(err)
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
if *drain {
io.Copy(io.Discard, resp.Body)
}
resp.Body.Close()
}
fmt.Printf("%d requests in %v, %d on a reused connection\n",
*n, time.Since(start).Round(time.Millisecond), reused)
}Save it as main.go in an empty directory and run it four ways. The last two hit the same host as the Go team's benchmark, with HTTP/2 turned off so you're testing HTTP/1:
go run main.go
go run main.go -drain
GODEBUG=http2client=0 go run main.go -url https://index.golang.org/index
GODEBUG=http2client=0 go run main.go -drain -url https://index.golang.org/indexThis is what we got on Go 1.26.2 on a MacBook in Europe:
100 requests in 22ms, 0 on a reused connection
100 requests in 9ms, 99 on a reused connection
100 requests in 8.09s, 0 on a reused connection
100 requests in 1.638s, 99 on a reused connectionWithout draining, not a single request reused a connection. Against a real HTTPS host that cost about 5x in wall time, the same ratio the Go blog reports. Our absolute numbers are higher than theirs because of the distance to the server and because /index returns a much larger body than a status endpoint. Run the same program under Go 1.27 and the no-drain case should come in close to the drained one, as long as the body fits under the 256 KB and 50 ms limits.



