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
Summary
The WS recv loop catches a bare
ConnectionClosedand unconditionally calls_handle_reconnect, which runs the fullws_max_retries=10backoff 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 inspectsConnectionClosed.rcvd.code/sent.codeto 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— bareexcept ConnectionClosedkalshi/ws/connection.py:140-200—_handle_reconnectretry loop, no code inspectionEvidence
Recommended fix
Classify close codes and short-circuit permanent ones. Reasonable taxonomy (from RFC 6455 + Kalshi conventions):
Implementation:
Tests:
test_close_4001_does_not_reconnect_and_raises_KalshiConnectionErrortest_close_1008_does_not_reconnecttest_close_1006_reconnectstest_close_4001_broadcasts_sentinels_so_consumer_iterators_terminateSeverity & category
high / correctness, ws, performance