Skip to content

WS: ERROR backpressure strategy silently terminates consumer iterator (no programmatic signal) #207

Closed

Description

@TexasCoding

Summary

When a queue with OverflowStrategy.ERROR (used for stateful channels: orderbook_delta, order_group_updates) fills, MessageQueue.put raises KalshiBackpressureError. The recv loop catches it, logs at ERROR, broadcasts sentinels, and breaks. The consumer iterator then sees StopAsyncIteration — indistinguishable from a clean shutdown.

The error is logged but there is no programmatic way for the consumer to detect that data loss occurred. Production strategies that rely on detecting backpressure to e.g. halt trading have no hook. The class docstring promises "use for stateful channels" implying the consumer will hear about the failure.

Location

  • kalshi/ws/client.py:211-219 — recv loop swallows
  • kalshi/ws/backpressure.py:60-67 — raise site

Evidence

# kalshi/ws/client.py:211-219
except (KalshiBackpressureError, KalshiSubscriptionError):
    logger.error("Fatal WS error in recv loop", exc_info=True)
    await self._broadcast_sentinels()
    break

Consumer code:

async for msg in stream:
    ...
# ← exits cleanly. No way to distinguish from a normal close.

Recommended fix

Two options, option B preferred:

Option A (lightweight)

Store the terminal exception on a KalshiWebSocket.terminal_error property and document that consumers should check it after async for exits:

async for msg in stream:
    ...
if ws.terminal_error is not None:
    raise ws.terminal_error

Option B (preferred — raises through the iterator)

Have MessageQueue.put_error(exc) put a special _ERROR sentinel carrying the exception. __anext__ then raises:

async def __anext__(self):
    item = await self._get()
    if item is _SENTINEL:
        raise StopAsyncIteration
    if isinstance(item, _Error):
        raise item.exc
    return item

Consumer code becomes natural:

try:
    async for msg in stream:
        ...
except KalshiBackpressureError:
    halt_strategy()

Document the new contract in docs/websockets.md and add tests:

  • test_error_strategy_overflow_raises_through_iterator
  • test_clean_close_still_raises_StopAsyncIteration

The current "StopAsyncIteration only" contract is hostile to safety-critical consumers.

Severity & category

medium / correctness, dx, ws

Activity

  1. 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

    enhancementNew feature or requestwsWebSocket-related

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions