On October 14, the Go team published a blog post confirming what Go 1.27 quietly shipped two months ago: the standard HTTP client now automatically fixes a performance bug that has silently punished Go programs for over a decade. Programs that closed HTTP response bodies without reading them were unknowingly abandoning TCP connections and paying the full handshake cost on every sequential request — up to 5x slower than they could be. According to the official Go blog post, the fix is already in your binary if you’re on Go 1.27. No code changes required.
The Bug That Looked Like Correct Go Code
In HTTP/1, responses travel sequentially over a single TCP stream. Before the client can send the next request on that connection, it must fully consume the current response body — the protocol has no way to locate where the next response begins otherwise. Consequently, Go’s net/http Transport maintains an idle connection pool; when a response body is abandoned unread, the Transport cannot return that connection to the pool. It creates a fresh one for the next request, paying TCP and TLS handshake costs each time.
The problem is that abandoning bodies looks completely correct. Consider the classic status-check pattern:
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 // body never read — new connection every call
}
This code is not wrong. The body is properly closed. However, in Go 1.26 and earlier, every call to getStatus abandons the connection and triggers a fresh TCP handshake. The same trap hits json.NewDecoder(resp.Body).Decode(&result) — after the decoder reads the closing brace, trailing newlines remain in the body unread. In fact, virtually every JSON API client in Go was quietly doing this, paying the handshake cost on every single request.
5x Faster Without Changing a Line
The Go 1.27 release notes measured 100 sequential HTTP/1 GET requests to index.golang.org: Go 1.26 took roughly 710 milliseconds; Go 1.27 takes about 150 milliseconds. That is a 4.7x speedup with zero code changes. The entire delta comes from connection reuse — eliminating the TCP and TLS handshakes that were previously required on each abandoned request. For services doing health checks, status polling, or any repeated HTTP calls at volume, this is not a marginal optimization.
Related: Cloudflare Traces Open Beta: Full Request Visibility
How Go 1.27 Fixed HTTP Connection Reuse
When Response.Body.Close() is called in Go 1.27+, the Transport asynchronously reads and discards any remaining body bytes — up to 256 KB or 50 milliseconds, whichever limit it hits first. If the body drains within those bounds, the connection goes back into the idle pool. Otherwise, the connection is abandoned exactly as before. The auto-drain runs in the background, so the calling goroutine is not blocked.
This eliminates the manual workaround experienced Go developers had been writing for years:
// The old workaround — no longer needed in Go 1.27+
defer func() {
io.Copy(io.Discard, io.LimitReader(resp.Body, 256<<10))
resp.Body.Close()
}()
That boilerplate is now dead code. Moreover, the 256 KB and 50 ms limits the Go team chose are the same conservative bounds the community had settled on through years of trial and error. Furthermore, Python’s requests, Java’s OkHttp, and Rust’s reqwest have all handled this transparently for years. The net/http package’s standard library finally standardized what careful developers were already doing manually — and applied it to every Go program automatically.
One Edge Case Worth Knowing
For most programs, the upgrade is fully transparent. However, if you set Transport.MaxIdleConns = 0 or create a new http.Client per request — patterns the Go docs explicitly discourage — behavior may differ. The release notes flag this directly: programs “improperly allowing an excessive amount of idle connections to linger” may see unexpected results. In that case, setting Transport.DisableKeepAlives = true opts out of connection reuse entirely. That said, hitting this edge case likely surfaces a deeper Transport misconfiguration worth fixing anyway.
Key Takeaways
- Go 1.27 automatically drains unread HTTP/1 response bodies when
Body.Close()is called, enabling connection reuse with no code changes - The fix produces a 4.7x speedup on sequential HTTP benchmarks — 710ms down to 150ms on 100 requests
- Common patterns that triggered the bug: checking only the status code, using
json.Decoderwithout consuming the full body, any code that closes without reading - If you maintained the
io.Copy(io.Discard, ...)workaround manually, you can now remove it — the standard library covers it - Only opt out with
Transport.DisableKeepAlives = trueif connection reuse genuinely hurts your specific workload













