Skip to content

WS: orderbook snapshot rebuilt and discarded on every frame (hot-path waste) #199

Description

@TexasCoding

Summary

In the WS recv hot path, _process_frame calls self._orderbook_mgr.apply_snapshot(snapshot) / apply_delta(delta) purely for side effect — the return value is not captured. But both methods unconditionally execute state.to_orderbook(), which:

  • runs sorted(self.yes.items()) and sorted(self.no.items()) → O(n log n) per side
  • allocates 2 * N OrderbookLevel Pydantic models per call

On a 500-level book, every single delta frame burns an O(n log n) sort and ~1000 Pydantic model allocations whose result is immediately discarded. At 1–10k orderbook_delta msg/s this is by far the dominant per-frame cost and creates significant GC pressure in long-running streamers. The user-facing OrderbookManager.get(ticker) already materializes on demand, so the work done inside the recv loop is wasted.

Location

  • kalshi/ws/client.py:285-292 — recv loop calls and discards return value
  • kalshi/ws/orderbook.py:76-123apply_snapshot / apply_delta always return materialized Orderbook

Evidence

# kalshi/ws/client.py:285-292 (recv hot path)
if msg_type == "orderbook_snapshot" and self._orderbook_mgr:
    snapshot = OrderbookSnapshotMessage.model_validate(data)
    self._orderbook_mgr.apply_snapshot(snapshot)   # return value discarded
    pre_validated = snapshot
elif msg_type == "orderbook_delta" and self._orderbook_mgr:
    delta = OrderbookDeltaMessage.model_validate(data)
    self._orderbook_mgr.apply_delta(delta)         # return value discarded
    pre_validated = delta
# kalshi/ws/orderbook.py:117-123 (inside apply_delta)
elif delta > 0:
    levels[price] = delta

return state.to_orderbook()   # ← O(n log n) sort + 2n OrderbookLevel allocations

Recommended fix

Split state mutation from snapshot materialization. Two options, either fine:

Option A — internal in-place variants (cleanest):

def _apply_snapshot_inplace(self, msg: OrderbookSnapshotMessage) -> None: ...
def _apply_delta_inplace(self, msg: OrderbookDeltaMessage) -> None: ...

def apply_snapshot(self, msg) -> Orderbook:
    self._apply_snapshot_inplace(msg)
    return self.get(msg.msg.market_ticker)

def apply_delta(self, msg) -> Orderbook | None:
    self._apply_delta_inplace(msg)
    state = self._books.get(msg.msg.market_ticker)
    return state.to_orderbook() if state else None

Have the recv loop call _apply_*_inplace directly.

Option B — opt-out kwarg:

def apply_delta(self, msg, *, materialize: bool = True) -> Orderbook | None:
    ...
    if not materialize:
        return None
    return state.to_orderbook()

Either way the recv path stops paying for the unused snapshot.

Add a perf benchmark (scripts/bench_ws_recv.py) that feeds 10k synthetic delta frames at varying book depths and reports frames/sec + GC counts — see separate "bench scripts missing" issue. Without a baseline this regression is invisible to CI.

Severity & category

high / performance, ws

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance / hot-path concernwsWebSocket-related

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions