Skip to content

WS: reconnect should fast-fail on permanent close codes (1008, 4001…) #197

Closed

Description

@TexasCoding

Summary

The WS recv loop catches a bare ConnectionClosed and unconditionally calls _handle_reconnect, which runs the full ws_max_retries=10 backoff loop. Permanent server-side closes — auth failure (4001), policy violation (1008), invalid frame (1003), message too big (1009) — will fail every attempt, but the SDK never inspects ConnectionClosed.rcvd.code / sent.code to short-circuit.

Result: 10 retries with capped 30s delay each (~3–5 minutes) of doomed reconnect attempts, sign executor doing pointless work, the consumer iterator stalled with no signal, and only THEN raising. For a trading bot this looks like an outage that should be a clean fail-fast.

Location

  • kalshi/ws/client.py:167-180 — bare except ConnectionClosed
  • kalshi/ws/connection.py:140-200 — _handle_reconnect retry loop, no code inspection

Evidence

# kalshi/ws/connection.py (loop has no code inspection)
for attempt in range(self._config.ws_max_retries):
    delay = self._config.retry_base_delay * (2**attempt) + random.uniform(0, 0.5)
    delay = min(delay, self._config.retry_max_delay)
    ...
    try:
        ...
        self._ws = await connect(...)
        return
    except Exception:
        logger.debug("Reconnect attempt %d failed", attempt + 1)
        continue

# kalshi/ws/client.py — bare catch
except ConnectionClosed:
    if not self._running:
        break
    await self._handle_reconnect()  # always reconnects, regardless of close code

Recommended fix

Classify close codes and short-circuit permanent ones. Reasonable taxonomy (from RFC 6455 + Kalshi conventions):

Code Meaning Action
1000 Normal closure Don't reconnect; consumer exited
1001 Going away (server restart) Reconnect
1002 Protocol error Don't reconnect — raise
1003 Invalid frame Don't reconnect — raise
1006 Abnormal closure (no Close frame) Reconnect
1007 Invalid payload Don't reconnect — raise
1008 Policy violation Don't reconnect — raise
1009 Message too big Don't reconnect — raise
1010 Mandatory extension Don't reconnect — raise
1011 Server error Reconnect
4000–4999 App-specific Don't reconnect — raise (esp. 4001 auth)

Implementation:

PERMANENT_CLOSE_CODES = frozenset({1002, 1003, 1007, 1008, 1009, 1010}) | frozenset(range(4000, 5000))

except ConnectionClosed as e:
    code = (e.rcvd.code if e.rcvd else None) or (e.sent.code if e.sent else None)
    if code in PERMANENT_CLOSE_CODES:
        await self._broadcast_sentinels()
        raise KalshiConnectionError(
            f"WebSocket closed with permanent code {code}: {e.rcvd.reason if e.rcvd else ''}"
        ) from e
    if not self._running:
        break
    await self._handle_reconnect()

Tests:

  • test_close_4001_does_not_reconnect_and_raises_KalshiConnectionError
  • test_close_1008_does_not_reconnect
  • test_close_1006_reconnects
  • test_close_4001_broadcasts_sentinels_so_consumer_iterators_terminate

Severity & category

high / correctness, ws, performance

Activity

  1. added
    bugSomething isn't working
    wsWebSocket-related
    performancePerformance / hot-path concern
    on May 21, 2026
  2. added a commit that references this issue on May 21, 2026
    f5db1a1
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

    bugSomething isn't workingperformancePerformance / hot-path concernwsWebSocket-related

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions