python/cpython · error · RuntimeError
cannot switch state from {} to {}
Error message
cannot switch state from {} to {} What it means
Raised by _SSLProtocol._switch_state() as RuntimeError when the SSL state machine is asked to move to a state that the current state does not permit (the allowed transitions are the explicit UNINIT->HANDSHAKING->...->FLUSHING->SHUTDOWN chain plus a catch-all into CON_LOST). It is an internal invariant check: the event that caused the transition (data, EOF, or connection loss arriving in an unexpected order) does not fit the protocol's current phase.
Source
Thrown at Lib/asyncio/sslproto.py:530
allowed = True
elif (
self._state == SSLProtocolState.WRAPPED and
new_state == SSLProtocolState.FLUSHING
):
allowed = True
elif (
self._state == SSLProtocolState.FLUSHING and
new_state == SSLProtocolState.SHUTDOWN
):
allowed = True
if allowed:
self._state = new_state
else:
raise RuntimeError(
'cannot switch state from {} to {}'.format(
self._state, new_state))
# Handshake flow
def _start_handshake(self):
if self._loop.get_debug():
logger.debug("%r starts SSL handshake", self)
self._handshake_start_time = self._loop.time()
else:
self._handshake_start_time = None
self._set_state(SSLProtocolState.DO_HANDSHAKE)
# start handshake timeout count down
self._handshake_timeout_handle = \
self._loop.call_later(self._ssl_handshake_timeout,
self._check_handshake_timeout)View on GitHub (pinned to bc6749cc3b)
Solutions
- Upgrade to the newest patch release of your Python version — multiple 'cannot switch state' races in sslproto have been fixed upstream.
- If you wrap transports/protocols, ensure each event (connection_made, data_received, eof_received, connection_lost) is delivered at most once and in order per connection.
- Avoid touching _SSLProtocol internals; use only public APIs (create_connection, start_tls, close()).
- Capture a traceback when it occurs and check whether it follows an aborted handshake (handshake timeout + concurrent close) — restructure code to await handshake completion before closing.
Example fix
// before: closing while handshake is still settling can race the state machine
conn = await loop.create_connection(proto, 'h', 443, ssl=ctx)
conn.close() # immediate close during handshake window
// after: let the handshake finish (or fail) before tearing down
reader, writer = await asyncio.open_connection('h', 443, ssl=ctx,
ssl_handshake_timeout=10)
await writer.drain() if False else None
writer.close()
await writer.wait_closed() Defensive patterns
Strategy: try-catch
Validate before calling
# let the handshake settle before closing, avoiding shutdown races
reader, writer = await asyncio.open_connection(
'host', 443, ssl=ctx, ssl_handshake_timeout=10.0)
# ... use connection ...
writer.close()
await writer.wait_closed() Try / catch
try:
reader, writer = await asyncio.open_connection('host', 443, ssl=ctx)
except RuntimeError as e:
if 'cannot switch state' in str(e):
log.exception('SSL state machine inconsistency; retrying once on a new connection')
reader, writer = await asyncio.open_connection('host', 443, ssl=ctx)
else:
raise Prevention
- Keep Python patched; several sslproto state-machine races were fixed across releases.
- Avoid closing connections concurrently with an in-flight handshake; await the handshake first.
- If you wrap transports, guarantee each lifecycle event fires once and in order.
When it happens
Trigger: Out-of-order lifecycle events on an SSL connection: e.g. data_received or eof_received firing after the protocol already entered SHUTDOWN/CON_LOST, which usually stems from bugs in custom transports/loops replaying events, from misuse of the internal API, or from Python bugs in sslproto's state handling under aborted handshakes.
Common situations: Custom event loops or protocol wrappers re-delivering events; older CPython releases with known sslproto state-machine races (several were fixed across 3.6-3.12); force-closing connections during handshake combined with handshake timeouts; fuzz testing that sends EOF mid-handshake.
Related errors
- Creating _SSLProtocolTransport twice
- {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/a316e263013a4164.
Report an issue: GitHub.