Skip to content

fix: close stdout stream explicitly to prevent CLOSE_WAIT socket leak#712

Open
olegnazarov23 wants to merge 1 commit intoanthropics:mainfrom
olegnazarov23:fix/close-wait-socket-leak
Open

fix: close stdout stream explicitly to prevent CLOSE_WAIT socket leak#712
olegnazarov23 wants to merge 1 commit intoanthropics:mainfrom
olegnazarov23:fix/close-wait-socket-leak

Conversation

@olegnazarov23
Copy link
Copy Markdown

Summary

Fixes #665 — after a ClaudeSDKClient session ends, the Python process burns ~24% CPU indefinitely due to a leaked CLOSE_WAIT TCP socket keeping the event loop busy-spinning.

Root Cause

SubprocessCLITransport.close() closes stdin and stderr streams but never explicitly closes the stdout stream — it only sets self._stdout_stream = None. The underlying file descriptor remains registered in the event loop's kqueue/epoll selector.

When the subprocess exits, any TCP connections it held (e.g. to the Anthropic API) enter CLOSE_WAIT. Since the socket's FD is still registered, kqueue reports it as readable every poll cycle (EOF pending), causing asyncio to busy-spin instead of blocking.

Fix

Explicitly call aclose() on the stdout TextReceiveStream before nulling the reference. This deregisters the FD from the event loop and closes the underlying socket, breaking the spin cycle.

# Before (stdout FD leaked):
self._stdout_stream = None

# After (FD properly deregistered):
if self._stdout_stream:
    with suppress(Exception):
        await self._stdout_stream.aclose()
    self._stdout_stream = None

This follows the same pattern already used for stdin and stderr streams in the same method.

Test

Added test_close_explicitly_closes_stdout_stream — verifies that close() calls aclose() on the stdout stream and cleans up all stream references.

All 40 transport tests pass.

…anthropics#665)

When SubprocessCLITransport.close() runs, it closes stdin and stderr
streams but never explicitly closes the stdout stream — only sets it
to None. This leaves the underlying file descriptor registered in the
event loop's kqueue/epoll selector.

If the subprocess held open TCP connections (e.g. to the Anthropic
API), those sockets enter CLOSE_WAIT after the process exits. A
CLOSE_WAIT socket is permanently readable (EOF pending), so with its
FD still registered in kqueue, the event loop busy-spins at ~24% CPU
instead of blocking — indefinitely.

Fix: explicitly call aclose() on the stdout stream before setting it
to None, which deregisters the FD from the event loop and closes the
underlying socket.

Closes anthropics#665
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.

CLOSE_WAIT socket leak causes persistent CPU spin on macOS (kqueue) even when fully idle

1 participant