python/cpython · error · RuntimeError
Creating _SSLProtocolTransport twice
Error message
Creating _SSLProtocolTransport twice
What it means
Raised by _SSLProtocol._get_app_transport() as RuntimeError when a new _SSLProtocolTransport is requested after one was already created and released (i.e. _app_transport is None because it was closed/invalidated, but _app_transport_created is still True). The SSL protocol hands out exactly one application-level transport over its lifetime; asking for a second one indicates the protocol object is being driven after its transport was already consumed.
Source
Thrown at Lib/asyncio/sslproto.py:377
self._app_protocol_buffer_updated = app_protocol.buffer_updated
self._app_protocol_is_buffer = True
else:
self._app_protocol_is_buffer = False
def _wakeup_waiter(self, exc=None):
if self._waiter is None:
return
if not self._waiter.cancelled():
if exc is not None:
self._waiter.set_exception(exc)
else:
self._waiter.set_result(None)
self._waiter = None
def _get_app_transport(self):
if self._app_transport is None:
if self._app_transport_created:
raise RuntimeError('Creating _SSLProtocolTransport twice')
self._app_transport = _SSLProtocolTransport(self._loop, self)
self._app_transport_created = True
return self._app_transport
def _is_transport_closing(self):
return self._transport is not None and self._transport.is_closing()
def connection_made(self, transport):
"""Called when the low-level connection is made.
Start the SSL handshake.
"""
self._transport = transport
self._start_handshake()
def connection_lost(self, exc):
"""Called when the low-level connection is lost or closed.
View on GitHub (pinned to bc6749cc3b)
Solutions
- Audit for double TLS upgrades: call start_tls()/upgrade at most once per connection; create a fresh connection for a second TLS layer.
- Ensure you never use the SSL transport or protocol object after connection_lost/close — drop all references in your protocol's connection_lost().
- Upgrade Python to the latest patch release — several sslproto lifecycle races have been fixed over time.
- If you maintain a custom loop/transport wrapper, verify connection_made/_get_app_transport are invoked exactly once per _SSLProtocol instance.
Example fix
// before: double upgrade on one connection tls_transport = await loop.start_tls(raw_transport, protocol, ctx) tls_transport2 = await loop.start_tls(tls_transport, protocol, ctx2) # re-driving the same protocol can hit 'Creating _SSLProtocolTransport twice' // after: single TLS layer per connection; reconnect for another layer tls_transport = await loop.start_tls(raw_transport, protocol, ctx) # need another cert context -> open a new connection with ssl=ctx2
Defensive patterns
Strategy: validation
Validate before calling
# enforce one TLS upgrade per connection at the application layer
upgraded = set()
async def upgrade_once(conn_id, transport, protocol, ctx):
if conn_id in upgraded:
raise RuntimeError(f'connection {conn_id} already TLS-upgraded')
upgraded.add(conn_id)
return await loop.start_tls(transport, protocol, ctx) Try / catch
try:
tls_transport = await loop.start_tls(transport, protocol, ctx)
except RuntimeError as e:
if '_SSLProtocolTransport twice' in str(e):
log.error('protocol already consumed by a previous TLS layer; reconnecting')
tls_transport = None # signal caller to open a fresh connection
else:
raise Prevention
- Call start_tls() at most once per connection; open a new connection for further layers.
- Drop references to transports/protocols in connection_lost so they cannot be reused.
- Stay on public asyncio APIs; do not drive _SSLProtocol lifecycle methods yourself.
When it happens
Trigger: Internal/library-level misuse: calling start_tls() machinery or _SSLProtocol lifecycle methods after the previous app transport was closed — e.g. performing a TLS upgrade twice on the same underlying protocol object, or an event loop/protocol wrapper re-triggering connection_made after connection_lost. Not reachable through the documented public transport API.
Common situations: Bugs in custom event loops or protocol wrappers that replay connection events; double start_tls upgrades on one connection; older Python versions with lifecycle races during aborted handshakes; third-party libraries (uvloop-era ported code) poking at _SSLProtocol internals.
Related errors
- cannot switch state from {} to {}
- {htmldir!r} is not a Sphinx HTML output directory
- Object type mismatch in limited API annotation for {name}: {
- deprecated-removed:: second argument cannot be `next`
- Socket cannot be of type SSLSocket
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/4bf9597090e9bfd1.
Report an issue: GitHub.