Skip to content

asyncio.wait_for around connection.recv() allocates a TimerHandle per frame #356

Description

@TexasCoding

Problem

Each iteration of _recv_loop wraps the raw socket recv() in asyncio.wait_for(...) with a 50 ms deadline so _pause_pending / _running get re-checked on quiet channels. CPython's wait_for registers a TimerHandle for the deadline, sets up a cancellation callback, wraps the awaitable in a task on 3.10 (and an asyncio.timeout() context manager on 3.11+ — still allocates an internal handle), then tears the timer down whenever the inner coroutine completes — even when it completes via a frame arriving in microseconds.

On a busy channel with 5–10 k frames/second the recv loop is allocating that many TimerHandles and cancellations per second. #245 explicitly traded shield/Task per-frame allocation for this poll, but the per-frame timer cost is non-zero and the comment on _RECV_POLL_S undersells it ("adds no overhead in the hot path").

Evidence

  • kalshi/ws/client.py:284-291:
try:
    # Poll with a short timeout so ``_pause_pending`` and
    # ``_running`` get re-checked on quiet channels. On a
    # busy channel the timeout is cancelled before firing,
    # so polling adds no overhead in the hot path.
    raw = await asyncio.wait_for(
        self._connection.recv(), timeout=_RECV_POLL_S,
    )
  • kalshi/ws/client.py:65: _RECV_POLL_S: float = 0.05

Suggested fix

Two options:

  • Cheap (docs only): amend the _RECV_POLL_S comment to acknowledge the per-frame TimerHandle allocation cost and explain why the cooperative pause approach was still chosen over shield/Task pairs.
  • Real fix: replace per-frame wait_for with a single long-lived recv_task = asyncio.ensure_future(self._connection.recv()) paired with an asyncio.Event that _pause_recv_loop sets. Race them via asyncio.wait({recv_task, pause_event.wait()}, return_when=FIRST_COMPLETED). The recv_task is reused on the next iteration (or recreated only after a frame is consumed) so allocation amortizes across many frames.

Validate with scripts/bench_ws_recv.py on a high-rate orderbook_delta scenario and only land the real fix if the bench shows a meaningful improvement.

Source

Round-3 independent audit (reviewer: performance).

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