python/cpython · error · RuntimeError

cannot switch state from

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.

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.

Appendix: 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)