deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Go 1.27 drains unread HTTP/1 response bodies, but connections still die past 256 KB

Go 1.27's net/http now drains unread HTTP/1 response bodies on Close so connections return to the idle pool, but only when the response is within 256 KB and the drain finishes inside 50 ms.

Go 1.27 drains unread HTTP/1 response bodies, but connections still die past 256 KB

Go 1.27 changes how the standard library's HTTP client handles HTTP/1 response bodies that a program closes without reading to the end. Instead of discarding the connection, net/http.Transport now drains the leftover bytes itself so the connection can return to the idle pool, but only within tight limits: at most 256 KB, and at most 50 milliseconds.

The change, examined in a dev.to post by Gabriel Anhaia, targets one of the most common notes in Go code review: add io.Copy(io.Discard, resp.Body) before resp.Body.Close() so the connection is reusable. Go 1.27 moves that habit into the standard library, and the limits determine where connections still die.

Why the drain matters

HTTP/1 sends responses back to back over a single TCP connection, and a client cannot send its next request until it has consumed the current body to the end, as defined by the Content-Length or Transfer-Encoding headers. On Go 1.26 and earlier, closing a body early meant the transport abandoned the connection and dialed a new one, with a fresh TCP handshake and, for HTTPS, a fresh TLS handshake.

The Go team's post "Better HTTP/1 connection reuse", published 14 October 2026 by Nicholas Husin and Damien Neil and cited by Anhaia, includes a benchmark: 100 sequential status-only GETs to index.golang.org, forced onto HTTP/1 with GODEBUG=http2client=0, took about 710 ms on Go 1.26 and about 150 ms on Go 1.27 on the author's machine. The program was unchanged; only the standard library behaved differently.

The old failure was easy to trigger by accident. As Anhaia points out, a json.Decoder can stop reading just before a JSON body's trailing newline, and a single unread byte was enough to lose the connection.

The exact rule

The Go 1.27 release notes describe the new drain as working up to a conservative limit, and issue golang/go#77370 defines it: 256 KB or 50 milliseconds, whichever is reached first. On the 1.27 release branch, the decision to attempt a drain is a single line in transport.go, tryDrain := alive && resp.ContentLength <= maxPostCloseReadBytes, and Anhaia draws out four consequences from it.

First, the check uses the declared Content-Length of the whole response, not the number of bytes left unread. A 300 KB body is never drained, even if the client read all but the final 100 bytes, which is the case where the promise of automatic draining is most misleading.

Second, a chunked response has a ContentLength of -1, so it passes the check. The drain then copies at most 256 KB plus one byte in a goroutine, and keeps the connection only if it reaches EOF.

Third, the drain gives up after 50 ms, so a server that pauses mid-body still costs the connection.

Fourth, if the request or the response carried Connection: close, no drain runs at all.

A related fix, tracked as CL 741222, means Close returns before the drain finishes, so draining never blocks the caller, though a subsequent request can occasionally race the still-running drain and dial a new connection anyway.

HTTP/2 is unaffected by any of this, because its multiplexed streams mean an unread body on one stream does not block the others.

Verifying it yourself

Anhaia's demonstration counts connections from the server side, where they arrive: a hook on http.Server.ConnState increments a counter on each StateNew event, and the program runs 100 GETs with configurable body size, bytes read, chunked encoding and mid-body stalls. The expected results follow from the rule above. A 4 KB body left entirely unread should reuse connections on 1.27, while a 300 KB body with only 100 bytes left unread, a 400 KB chunked body, or a 100 ms server stall should still produce roughly one connection per request on both versions. Anhaia notes he did not run these himself; the expectations come from the code.

What to do with existing code

For modules that build only with Go 1.27 or later, Anhaia's advice is to delete the io.Copy(io.Discard, resp.Body) lines and keep the deferred Close, matching the Go team's guidance to simply close the body. He also observes that an unbounded io.Copy was always a liability: aimed at a multi-gigabyte download you intended to abandon, it would read the whole thing on your goroutine. No GODEBUG setting is listed in the release notes for the new behavior, so it ships with 1.27 unconditionally.

Why it matters

Connection reuse is one of the largest hidden performance levers in an HTTP client, and the Go team's own benchmark shows a better-than-fourfold speedup on a trivial request pattern once reuse works. For teams upgrading, the change removes boilerplate that most Go services carry. But the 256 KB and 50 ms ceilings mean partially read large responses, chunked responses beyond 256 KB, and slow servers still tear connections down. Knowing exactly where the automatic behavior stops is what separates code that keeps its connection pool warm from code that silently re-handshakes on every request.

  • #golang
  • #http
  • #networking
  • #performance
  • #standard-library

Related posts