Skip to content

WS perf: subscribe_book iterator re-materializes the entire orderbook on every delta #244

Closed

Description

@TexasCoding

Summary

Every orderbook_delta frame consumed via the high-level subscribe_book(ticker=...) API pays O(n log n) sort on both sides plus 2N Pydantic OrderbookLevel validations + 1 Orderbook validation per delta. For a 200-level book at 100 deltas/sec this is ~40k Decimal validations/sec per ticker, completely defeating the #199 in-place hot path the recv loop carefully preserves.

Location

  • kalshi/ws/client.py:850-856 — _OrderbookIterator.__anext__
  • kalshi/ws/orderbook.py:46-58 — _BookState.to_orderbook (sort + validate)
  • kalshi/ws/orderbook.py:186-194 — OrderbookManager.get

Evidence

# client.py _OrderbookIterator
async def __anext__(self) -> Orderbook:
    await self._stream.__anext__()
    book = self._mgr.get(self._ticker)
    ...

# orderbook.py _BookState.to_orderbook (called by .get):
yes_levels = [
    OrderbookLevel(price=price, quantity=qty)
    for price, qty in sorted(self.yes.items())
]
no_levels = [
    OrderbookLevel(price=price, quantity=qty)
    for price, qty in sorted(self.no.items())
]
return Orderbook(ticker=self.ticker, yes=yes_levels, no=no_levels)

Recommended fix

Yield a lightweight view (per-side dict[Decimal, Decimal] snapshot or a frozen sorted tuple cached on _BookState and invalidated on mutation) instead of constructing a fresh validated Orderbook model every frame; only materialize on explicit mgr.get(). Add a bench parametrized on N (levels) × frames/sec.

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