FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

asyncio leaks transport/protocol/SSLObject on failed TLS handshake: `SSLError` traceback forms a reference cycle · Issue #159049 · python/cpython · GitHub

Repository navigation

asyncio leaks transport/protocol/SSLObject on failed TLS handshake: SSLError traceback forms a reference cycle #159049

Closed

Description

Bug report

When a proactive TLS handshake performed by loop.create_connection() fails (e.g. with ssl.SSLCertVerificationError: CERTIFICATE_VERIFY_FAILED).
The objects are only freed by the cyclic garbage collector, never by reference counting.
The same scenario under uvloop shows no such cycle.
If you clear the traceback from the exception object, the memory stops flowing.

python version:
Python 3.14.3 free-threading build (main, Mar 20 2026, 00:34:34) [Clang 22.1.1 ]
Python 3.14.8 free-threading build (main, Oct 1 2026, 21:01:25) [Clang 22.1.3 ]

import asyncio
import ssl
import gc
# import uvloop


gc.disable()


class ClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        print("Connection Made")

    def data_received(self, data):
        print("Data received: {!r}".format(data.decode()))

    def connection_lost(self, exc):
        print("The server closed the connection")


async def conn(ssl_ctx):
    loop = asyncio.get_running_loop()
    await loop.create_connection(
        lambda: ClientProtocol(),
        host="selfsigned-server",
        port=443,
        ssl=ssl_ctx,
        ssl_handshake_timeout=1,
    )


async def run():
    max_iter = 1000
    ssl_ctx = ssl.create_default_context()
    while max_iter:
        print(f"attempt left: {max_iter}    ", end="\r")
        max_iter -= 1
        try:
            await conn(ssl_ctx)
        except Exception as e:
            # e.__traceback__ = None
            pass
        await asyncio.sleep(0.01)


if __name__ == "__main__":
    # uvloop.run(run())
    asyncio.run(run())

CPython versions tested on:

3.14

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Oct 9, 2026
  2. rus1g commented on Oct 9, 2026

    Author

    Check the small monkeypatch, after which memory leaks stopped. I hope this will help in finding the problem.

    1. Added del waiter to _create_connection_transport — this solved the problem almost completely, but not fully.
    2. Added self._read_ready_cb = None to _call_connection_lost — this solved the problem completely.
    from asyncio.base_events import BaseEventLoop
    from asyncio.selector_events import _SelectorSocketTransport
    
    async def _create_connection_transport(
        self: BaseEventLoop,
        sock,
        protocol_factory,
        ssl,
        server_hostname,
        server_side=False,
        ssl_handshake_timeout=None,
        ssl_shutdown_timeout=None,
    ):
    
        sock.setblocking(False)
    
        protocol = protocol_factory()
        waiter = self.create_future()
        if ssl:
            sslcontext = None if isinstance(ssl, bool) else ssl
            transport = self._make_ssl_transport(
                sock,
                protocol,
                sslcontext,
                waiter,
                server_side=server_side,
                server_hostname=server_hostname,
                ssl_handshake_timeout=ssl_handshake_timeout,
                ssl_shutdown_timeout=ssl_shutdown_timeout,
            )
        else:
            transport = self._make_socket_transport(sock, protocol, waiter)
    
        try:
            await waiter
        except:
            transport.close()
            del waiter
            raise
    
        return transport, protocol
    
    
    BaseEventLoop._create_connection_transport = _create_connection_transport
    _orig_ccl = _SelectorSocketTransport._call_connection_lost
    
    
    def _call_connection_lost(self, exc):
        self._read_ready_cb = None
        _orig_ccl(self, exc)
    
    
    _SelectorSocketTransport._call_connection_lost = _call_connection_lost
  3. sergey-miryanov commented on Oct 9, 2026

    Contributor

    @deadlovelll You may be interested in #142516

  4. rus1g commented on Oct 10, 2026

    Author

    3. Hey, it's not really a leak, because sooner gc will collect those objects. But, if you don't mind i can work on this

    I completely agree that this isn’t exactly a leak. But in a high‑load application, you want to avoid running the gc. It would be great if you could resolve this case.

  5. added a commit that references this issue on Oct 18, 2026
  6. added 2 commits that reference this issue on Oct 19, 2026
  7. moved this from Todo to Done in asyncioon Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL