What Go 1.27's HTTP/1 body draining means for you
Go 1.27 now drains unread HTTP/1 response bodies for you, so connections get reused. Here's why that matters and which helper code you can delete.
Go 1.27 changes what happens when you close an HTTP/1 response body you did not finish reading. According to the Go Blog, net/http.Transport now drains the leftover body in the background, for up to 50 ms or 256 KB. That lets the connection be reused instead of thrown away. In the post’s own benchmark, 100 status-code-only requests to index.golang.org took about 710 ms on Go 1.26 and about 150 ms on Go 1.27, measured on the author’s machine.
The background you need
Opening a connection is the expensive part. An HTTPS request over a fresh connection first needs a TCP handshake. TCP is the transport that carries HTTP. Then it needs a TLS handshake, which sets up encryption. Each handshake costs one or more round trips. A round trip is the time for a message to reach the server and for a reply to come back, often written as RTT. The Go Blog’s diagram assumes 100 ms RTT. In that case, setting up the connection takes 300 ms, while the request itself can take only a little over one round trip. The post says the gap is still noticeable against index.golang.org. That service is distributed worldwide and supports TLS 1.3, where the TLS handshake can finish in one round trip.
Connection reuse is how clients avoid that cost. Go’s http.Transport keeps finished connections in a pool of idle connections. When you make the next request to the same host, it can pick one up and skip both handshakes. This only works if the connection is in a clean state.
In HTTP/1, responses share one byte stream. HTTP/1.0 and HTTP/1.1 send requests and responses one after another on the same connection. Two responses arrive as a single continuous run of bytes. http.Get reads only the status line and headers, then returns. The body stays on the wire until you read resp.Body. The client finds where a body ends from the Content-Length header, which gives a byte count, or the Transfer-Encoding header, which marks a chunked body. So it can only find the start of the next response after the current body has been read to the very end.
Think of a cassette tape. To reach the next song, you have to get past the current one. The analogy breaks in one place. A tape can fast-forward cheaply, but a network connection cannot skip ahead. Every remaining byte has to be downloaded and thrown away.
HTTP/2 works differently. It multiplexes: it splits many requests into interleaved frames on one connection. An unread body on one stream does not block the others. The Go Blog says HTTP/2 does not have this problem.
GODEBUG is an environment variable that toggles specific Go runtime and library behaviors. The post’s benchmark sets GODEBUG=http2client=0 to force the client onto HTTP/1.
What actually changed in Go 1.27
First, what was already true. Through Go 1.26, closing a body with unread bytes meant the Transport could not reuse that connection. It abandoned the connection and opened new ones for the next requests. The Go Blog calls this a source of “silent performance degradation in seemingly innocuous code”.
The common trigger is code that only checks resp.StatusCode, while the server sends a small body you never read. A subtler case comes from encoding/json.Decoder. It stops reading at the closing brace of a JSON object. If the server ends the response with a newline, that one byte stays unread, and a healthy connection gets abandoned.
Developers worked around this by draining bodies before closing them. The Go Blog walks through why that is harder than it looks:
- Copying the whole body to
io.Discardworks for small bodies. For a large body, opening a new connection can be faster. - Capping the drain at 256 KB fixes size, but a slow, trickling body can still hold you up.
- Adding a 50 ms timeout fixes time, but now
Closecan block for up to 50 ms. - Draining in a background goroutine fixes blocking, at the cost of more tricky code.
The post adds that, as the habit spread, people sometimes drained HTTP/2 bodies too. That is unnecessary.
Here is what’s new. According to the Go Blog, starting in Go 1.27, net/http.Transport does all of this itself. It drains an unread HTTP/1 response body asynchronously for up to 50 ms or 256 KB. It also skips the drain when the response includes a Connection: close header, because that connection will not be reused anyway. The post says this behavior might evolve in future versions of Go.
The official advice is now to simply close bodies once you no longer need them, using defer resp.Body.Close().
What it means for developers who don’t read every release note
The 50 ms / 256 KB drain is the detail that matters most. It turns a common, invisible slowdown into a non-issue, but only within those limits.
- You may get faster for free. This applies if your services speak HTTP/1 and close bodies early. Typical cases are plain-HTTP calls between internal services, servers that don’t offer HTTP/2, or clients you have forced onto HTTP/1. For those, upgrading to Go 1.27 should cut the number of new connections. How much depends on your latency and traffic. The post’s roughly 5x figure comes from one benchmark on one machine.
- You can likely delete drain helpers. If you copied a
drainAndCloseBodyfunction into your codebase, the Go Blog’s recommendation is to just callClose. Fewer goroutines and timers to reason about. - The limits still apply. A body larger than 256 KB, or one that takes longer than 50 ms to arrive, will not finish draining. In those cases the connection still won’t be reused. If you need reuse for large responses, you still have to read them yourself.
- HTTP/2 traffic is unchanged. It never needed draining.
To see the effect, watch your outbound connection and TLS handshake counts after the upgrade. Also check future release notes, since the Go team says the behavior may change.
What we don’t know yet
- Whether you can tune the limits. The post describes 50 ms and 256 KB but does not say whether either can be configured.
- How old helpers interact with the new behavior. The post recommends dropping them. It does not say whether keeping them costs anything.
- Real-world gains. The post gives one benchmark against a low-latency, worldwide service. It does not publish numbers for slower links, high-concurrency workloads, or large bodies.
- Future changes. The Go team says the behavior might evolve as other possible enhancements become apparent. It does not say what those are.
FAQ
Do I need to change my code for Go 1.27?
Does this affect HTTPS requests that use HTTP/2?
Why doesn't Go just read the whole body every time?
Related tools
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-09-02
The Best Monitors for Programming: PPI Over Size
A 27-inch 4K panel is 163 PPI; 1440p at the same size is 109. Why that gap decides text quality on macOS, plus five panels worth the money.
2026-09-02
The Best Mechanical Keyboards for Developers
Five keyboards ranked by the firmware you can actually remap — QMK, VIA, ZMK — plus the free software fix that makes buying one unnecessary.
2026-09-02
Best Coding Interview Books: 5 Picks and the Order to Buy
CtCI, Elements of Programming Interviews, Grokking Algorithms, Alex Xu's System Design Interview and Skiena: the job each book does and the order to buy.
2026-08-21
Your Bot Filter Misses Crawlers That Send a Referer
A no-referer heuristic passed 234 of 506 affiliate clicks as human. A country exclusion cut the same set to 117.
2026-08-21
Zero Social Referrers in 30 Days Across 35 Browser Games
Two causes: referrer stripping made the loop unmeasurable, and an await before navigator.share() failed silently on iOS Safari.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.