You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
LDClient.close() leaks components when one fails to stop, and is not idempotent #495
LDClient.close() is documented as "Releases all threads and network connections used by the LaunchDarkly client", but it cannot honour that in two situations.
Found while investigating #493 — a production incident where close() hung and left worker pods alive for hours to days, each holding a concurrency slot. These two are separate defects in the same method, found by auditing the shutdown path afterwards. I have not observed either in our own production, but both reproduce reliably.
There is no error handling. If the first raises, the data system and the big segment store manager are never stopped.
This is not contrived: two of these three reach code the SDK does not control — _data_system.stop() reaches the eventsource client, and __big_segment_store_manager.stop() calls stop() on the application's own BigSegmentStore implementation. A third-party store raising on shutdown is entirely ordinary. The caller is then left with leaked threads and connections belonging to a client it believes is closed.
2. close() is not idempotent
LDClient has no closed-flag, so a second close() runs the whole sequence again. That calls stop() a second time on the application's BigSegmentStore, and under FDv2 calls store.close() twice. DefaultEventProcessor.stop() is the only layer that self-guards.
Double-close is easy to reach by accident — an explicit close() inside a with block does it, as does any cleanup path that can run twice.
To reproduce
PR #498 adds two failing tests (marked xfail(strict=True) so CI stays green). Run them with --runxfail to see the real failures.
For the idempotency case the recorded shutdown log is:
I have deliberately not proposed a fix, since the error-propagation semantics (whether close() should still raise after releasing everything) is a design call for the SDK team. The tests pin the leak, not the exception behaviour.
One more way to reach §2 (non-idempotency), from production use.
A double close() can happen without the application ever calling close() twice, because set_config() closes the previous client for you.
What we hit:
our shutdown calls client.close() — that client is now closed
the SDK's module-global __client still points at the same closed object
something later calls set_config() (we re-initialise), which runs old_client.close() on it
Second close, and nothing in application code asked for it. That makes the accidental case stronger than the with-block example in the report: here the second call happens inside the SDK, so it isn't visible in the application at all. Any app that closes its client and later reconfigures hits it.
Activity
aviadr1 commentedon Aug 16, 2026
One more way to reach §2 (non-idempotency), from production use.
A double
close()can happen without the application ever callingclose()twice, becauseset_config()closes the previous client for you.What we hit:
client.close()— that client is now closed__clientstill points at the same closed objectset_config()(we re-initialise), which runsold_client.close()on itSecond close, and nothing in application code asked for it. That makes the accidental case stronger than the
with-block example in the report: here the second call happens inside the SDK, so it isn't visible in the application at all. Any app that closes its client and later reconfigures hits it.Confirmed on 9.16.0 — two
close()calls give:A closed-flag would fix this case as well as the explicit one.
jsonbailey commentedon Aug 21, 2026
@aviadr1 Thank you for raising the issue and the PRs. We will look at this and provide an update as soon as we can.