From #106 polish backlog. Severity: low under normal load, medium under burst (batch order placement, dense WS reconnects).
Code
kalshi/auth.py:162 (sign_request) calls cryptography's RSAPrivateKey.sign(...) synchronously. RSA-PSS over a 2048-bit key is ~1–3 ms of pure CPU per signature on a modern machine; with a 4096-bit key it's closer to 10 ms.
Every authenticated REST request and every WS connect runs this on the asyncio event loop, blocking the loop for the duration. Under sustained high request rates (batch_create_v2, repeated WS reconnects after a network blip) this measurably stalls competing coroutines on the loop.
Fix
Wrap the sign(...) call in asyncio.to_thread(...) on the async path. The sync transport path stays inline — no event loop, no problem.
# In the async _base_client signing call site:
headers = await asyncio.to_thread(self._auth.sign_request, method, path, ts_ms)
Notes
cryptography's C-backed sign(...) releases the GIL, so executor offload is genuine parallelism for concurrent request bursts.
asyncio.to_thread uses the default executor (default min(32, cpu+4) threads since 3.8). Sufficient.
- WS auth signs once per connect, so the gain there is bounded; the real win is REST burst paths.
Acceptance
sign_request is no longer called directly from async REST / async WS connect paths.
- Microbench in
tests/ (or a one-off script kept under scripts/): 1000 × sign_request under asyncio.gather does not block competing coroutines. Measure with a 100 Hz asyncio.sleep(0) ticker — pre-fix shows ms-scale schedule gaps clustered around sign calls, post-fix shows tight scheduling.
- No regression in sync transport tests (
tests/test_async_orders.py etc.).
tests/test_auth.py still green; the sync sign_request API is unchanged.
From #106 polish backlog. Severity: low under normal load, medium under burst (batch order placement, dense WS reconnects).
Code
kalshi/auth.py:162(sign_request) callscryptography'sRSAPrivateKey.sign(...)synchronously. RSA-PSS over a 2048-bit key is ~1–3 ms of pure CPU per signature on a modern machine; with a 4096-bit key it's closer to 10 ms.Every authenticated REST request and every WS connect runs this on the asyncio event loop, blocking the loop for the duration. Under sustained high request rates (
batch_create_v2, repeated WS reconnects after a network blip) this measurably stalls competing coroutines on the loop.Fix
Wrap the
sign(...)call inasyncio.to_thread(...)on the async path. The sync transport path stays inline — no event loop, no problem.Notes
cryptography's C-backedsign(...)releases the GIL, so executor offload is genuine parallelism for concurrent request bursts.asyncio.to_threaduses the default executor (defaultmin(32, cpu+4)threads since 3.8). Sufficient.Acceptance
sign_requestis no longer called directly from async REST / async WS connect paths.tests/(or a one-off script kept underscripts/):1000 × sign_requestunderasyncio.gatherdoes not block competing coroutines. Measure with a 100 Hzasyncio.sleep(0)ticker — pre-fix shows ms-scale schedule gaps clustered around sign calls, post-fix shows tight scheduling.tests/test_async_orders.pyetc.).tests/test_auth.pystill green; the syncsign_requestAPI is unchanged.