Repository navigation
Description
Problem
When the Kalshi server rate-limits a fleet of clients (e.g. an HFT desk running N processes that all subscribed to the same WS feed and now retry the same REST endpoint), every process receives the same Retry-After: 2 header and sleeps for an identical 2.0 seconds. They all reconnect on the same tick and re-trigger the throttle — exactly the thundering-herd shape that the surrounding _compute_backoff uses AWS Full Jitter to defeat.
The _compute_backoff docstring explicitly motivates Full Jitter ("spreads colliding retries evenly across the full backoff window, so parallel clients that hit a rate limit at the same time don't bunch up in a narrow sub-window"), but the Retry-After branch disables jitter on the one code path most likely to produce synchronized clients in the first place.
Evidence
kalshi/_base_client.py:145-155— Full Jitter rationale:
def _compute_backoff(attempt: int, config: KalshiConfig) -> float:
"""Exponential backoff with AWS "Full Jitter".
Spreads colliding retries evenly across the full backoff window, so
parallel clients that hit a rate limit at the same time don't bunch up
in a narrow sub-window ...
"""
capped = min(config.retry_base_delay * (2**attempt), config.retry_max_delay)
return float(random.uniform(0, capped))kalshi/_base_client.py:389-394— sync retry loop, no jitter on Retry-After:
if isinstance(error, KalshiRateLimitError) and error.retry_after is not None:
delay = min(error.retry_after, self._config.retry_max_delay)
else:
delay = _compute_backoff(attempt, self._config)kalshi/_base_client.py:600-604— async retry loop has the identical un-jittered branch.
Suggested fix
Keep Retry-After as a hard lower bound (honor the server's hint), then add jitter on top. One concrete shape:
if isinstance(error, KalshiRateLimitError) and error.retry_after is not None:
floor = min(error.retry_after, self._config.retry_max_delay)
extra = _compute_backoff(attempt, self._config)
delay = min(floor + extra, self._config.retry_max_delay)
else:
delay = _compute_backoff(attempt, self._config)This preserves the server-requested wait, never sleeps less than Retry-After, never exceeds retry_max_delay, and breaks phase-lock across a client fleet. Mirror on the async path. Document in docs/configuration.md next to the existing retry section.
Source
Round-3 independent audit (reviewer: http_transport).
Activity
fix(transport): Retry-After on 5xx/408 + jittered + success body cap (#…