Skip to content

Close the client's connections when EnableConnectionReuse is disabled - #426

Open
soluchok wants to merge 1 commit into
Danny-Dasilva:mainfrom
soluchok:upstream/close-connections-without-reuse
Open

soluchok wants to merge 1 commit into
Danny-Dasilva:mainfrom
soluchok:upstream/close-connections-without-reuse

Conversation

@soluchok

@soluchok soluchok commented Aug 14, 2026 •

Copy link
Copy Markdown

Problem

EnableConnectionReuse: false leaks a socket per request — and leaks harder than leaving reuse on.

With reuse disabled, getOrCreateClient skips the pool and calls createNewClient, so each Do() builds its own client and roundTripper and then drops it. Nothing closes what it opened. Reuse-on at least keeps reaching the same connection; reuse-off strands a fresh one every request.

Measured on v1.0.30, 10 requests to 10 hosts, counting the process's own sockets:

EnableConnectionReuse=true   ->  9 sockets left behind
EnableConnectionReuse=false  ->  8 sockets left behind   <- the option meant to avoid this

Change

Close the client's connections at the end of Do() when the client came from outside the pool.

EnableConnectionReuse=false builds a client outside the pool for a single
request and then drops it. Nothing closes what that client opened: the
roundTripper holding the connection goes out of scope still connected, with no
reference left anywhere to close it from.

So the option that exists to avoid holding connections open held them open
harder than leaving reuse on -- reuse at least reaches the same connection
again, where this stranded a fresh one per request. Measured before this
change, ten requests with reuse off left eight sockets behind, against nine
with it on.

Close them at the end of Do() when the client came from outside the pool. That
is safe precisely because it did: no other request shares it, and Do() has
already read the body to completion by then, so nothing is still reading. The
pooled path is untouched, where closing would pull connections out from under
concurrent callers.

Twelve requests with reuse off now leave nothing behind, and a caller that wants
a fresh TLS connection per request gets one for the cost of the handshake.
@smeinecke

Copy link
Copy Markdown

Thanks for tracking this down - we just hit the exact same failure mode in production: fd-per-request growth with EnableConnectionReuse: false until the process wedged on EMFILE (~18h to fill a 65k limit at ~1 req/s average).

One related spot that has the same leak shape: dispatcherAsync defers transport.CloseIdleConnections(hostPort), which keeps the just-served conn cached even when that client was created fresh for the request (getOrCreateClient with reuse off). Might be worth covering in the same change - closing all cached conns there when EnableConnectionReuse is off.

smeinecke added a commit to smeinecke/cycletls_python that referenced this pull request Oct 26, 2026
Port of Danny-Dasilva/CycleTLS#426. With enable_connection_reuse=false,
getOrCreateClient builds a fresh client outside the pool and Do() drops
it without closing what it opened - dialTLS caches the live conn into
rt.cachedConnections and nothing ever frees it. On HTTP/2 (the common
ALPN outcome) the ClientConn read/write loops pin the socket open, so
every request leaked a socket + 2 goroutines until EMFILE. HTTP/1 was
already covered by DisableKeepAlives; h2 has no such knob.

- CycleTLS.Do(): close all connections on the roundTripper when the
  client came from outside the pool. Safe: nothing shares it and the
  body is fully read by then. Covers every FFI entry point.
- dispatcherAsync: non-pooled clients now close all conns instead of
  keeping the just-served hostPort conn cached on a dropped client.
- roundTripper.CloseIdleConnections: also drain idle conns inside
  cached transports (graceful h2 GOAWAY) and drop transport entries
  that have no cached conn (http-scheme path was never cleaned).

Regression test drives 8 reuse-off requests against a local h2 server
and asserts goroutine/fd counts stay flat; fails on the unfixed code
(+24 goroutines, +16 fds).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants