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

  1. Upgrade to the newest patch release of your Python version — multiple 'cannot switch state' races in sslproto have been fixed upstream.
  2. 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.
  3. Avoid touching _SSLProtocol internals; use only public APIs (create_connection, start_tls, close()).
  4. 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

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


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/a316e263013a4164. Report an issue: GitHub.