· via Hacker News – Front Page (native)
Go 1.27 fixes HTTP/1 connection reuse, cutting repeated requests from 710ms to 150ms
A Go blog post shows Go 1.27 keeping HTTP/1 connections alive where Go 1.26 abandoned them, running a 100-request benchmark about five times faster with no code changes.

Go 1.27 reworks how the standard library's HTTP client reuses HTTP/1 connections, and the payoff is dramatic for a very common coding pattern. According to a post on the Go blog, a small program that calls http.Get 100 times against index.golang.org — reading only each response's status code — took roughly 710 milliseconds on Go 1.26 and about 150 milliseconds on Go 1.27 on the author's machine, with HTTP/2 disabled via GODEBUG=http2client=0. That is close to a fivefold improvement from a runtime upgrade alone. The post, which reached the Hacker News front page, explains why the gap is so large.
The cost of a fresh connection
The post walks through why opening a connection is expensive. A TCP handshake costs one round trip: SYN, SYN-ACK, ACK, with the client able to send its first TLS message alongside that final ACK. The TLS handshake then costs two more round trips under TLS 1.2, so the first HTTP request can only go out after three full round trips, plus the CPU time spent on the cryptographic handshake itself.
Those round trips dominate when latency is high. The post notes that cross-region links often reach or exceed 50 milliseconds one way, meaning 100 milliseconds per round trip; spending around 300 milliseconds on setup while the request itself needs barely more than one round trip is a poor trade. Even index.golang.org, which is distributed globally and supports TLS 1.3 (reducing the TLS handshake to one round trip), shows a very visible penalty for repeatedly churning through fresh connections.
Reuse is the standard answer, and the mechanics differ by protocol version. HTTP/1.0 clients and servers opt in informally with a Connection: keep-alive header. HTTP/1.1 connections are persistent by default unless one side sends Connection: close. HTTP/2 goes further, multiplexing many requests and responses over a single TCP connection.
How HTTP/1 reuse breaks
On HTTP/1, requests and responses sharing a connection are strictly sequential on the wire, arriving as one continuous byte stream. net/http returns from http.Get once it has parsed the status line and headers; the body is consumed lazily as the caller reads from it. Because the next response begins immediately after the current one ends, the client can only locate that boundary once the preceding body has been read to completion, as delimited by the Content-Length or Transfer-Encoding headers.
That is where the example bites. It closes the body without ever reading it, and through Go 1.26 that forces net/http to give up on the connection: unable to find the start of the next response, it discards the connection and pays the full setup cost for every subsequent request. HTTP/2 is immune, since its multiplexed streams mean an unread body on one stream does not block other requests on the same connection.
The failure mode is easy to trigger unintentionally. A client that only checks a status code will still receive whatever body the endpoint sends, such as a short JSON confirmation with a success flag and message. Even code that does read the body can fall short: encoding/.Decoder stops as soon as it sees the closing brace, so a server that terminates its JSON with a newline leaves exactly one byte unread — enough for a healthy connection to be thrown away.
The workarounds developers used
The accepted fix was to drain the body before closing it, copying the bytes into io.Discard. That is fine for small responses, but for a large unread body, opening a new connection can be faster than downloading and discarding the remainder, so the drain needs a size cap — the post uses 256 KB via io.CopyN. A server that trickles bytes in slowly adds a second constraint, a time limit of around 50 milliseconds, and to avoid blocking on close the drain must run asynchronously in the background, racing against subsequent requests. The post builds this sequence up step by step, and the escalating complexity of a correct manual solution is a good illustration of why the problem festered.
Why it matters
Go 1.27 addresses the trap: the same unread-body pattern from the benchmark now completes in about a fifth of the time, with no source changes required. This matters because the degradation was silent — services paying a fresh TCP and TLS handshake on every request looked correct and merely slow, with the cost concentrated exactly where round trips are most expensive, such as cross-region traffic. Anyone running HTTP/1 clients against internal APIs, or in environments where HTTP/2 is disabled, gets a meaningful latency and CPU reduction simply by upgrading. Draining response bodies before closing them remains good hygiene, but it is no longer the difference between a pooled connection and a new handshake every time.
- #golang
- #http
- #networking
- #performance
- #standard-library