Summary
The retry loop has no total wall-clock budget. With current defaults (timeout=30.0, max_retries=3, retry_max_delay=30.0), a single client.markets.list() can block the caller for ~180 seconds in the pathological case: each of 4 attempts may consume up to 30s of httpx timeout + up to 30s of Full-Jitter sleep.
For a trading SDK this is a livelock-class risk — a 30s+ stall on the hot path of an automated trader is functionally an outage. The retry policy needs either (a) a hard wall-clock cap that short-circuits remaining retries once the budget is exhausted, or (b) tighter defaults with explicit worst-case latency in the docs.
Location
kalshi/_base_client.py:77-87 — _compute_backoff
kalshi/_base_client.py:130-200 (sync) and :262-330 (async) — retry loops have no elapsed-time check
kalshi/config.py:47-50 — defaults
Evidence
# kalshi/config.py:47-50
timeout: float = DEFAULT_TIMEOUT # 30.0
max_retries: int = DEFAULT_MAX_RETRIES # 3
retry_base_delay: float = 0.5
retry_max_delay: float = 30.0
Worst-case computation for a single call:
- 4 attempts × 30s httpx timeout = 120s
- 3 retry sleeps × up to 30s Full Jitter = up to 90s
- Total: up to 210s of blocking on one call
Recommended fix
Add total_timeout: float | None = None (or request_deadline) to KalshiConfig. In the retry loop, check time.monotonic() - start > total_timeout before each sleep AND before each new attempt, short-circuiting with the last error.
Reasonable defaults:
total_timeout = 60.0 so the absolute worst case is bounded
- OR lower
retry_max_delay default to ~5s (gives ~45s worst case)
Document the budget calculation in docs/configuration.md so operators can pick saner values for their workload.
Add tests:
test_total_timeout_short_circuits_retries — mock time.monotonic to exceed budget after 2 attempts; assert the 3rd never fires.
test_total_timeout_None_preserves_legacy_behavior — unchanged retry count when total_timeout=None.
Severity & category
high / correctness
Summary
The retry loop has no total wall-clock budget. With current defaults (
timeout=30.0,max_retries=3,retry_max_delay=30.0), a singleclient.markets.list()can block the caller for ~180 seconds in the pathological case: each of 4 attempts may consume up to 30s of httpx timeout + up to 30s of Full-Jitter sleep.For a trading SDK this is a livelock-class risk — a 30s+ stall on the hot path of an automated trader is functionally an outage. The retry policy needs either (a) a hard wall-clock cap that short-circuits remaining retries once the budget is exhausted, or (b) tighter defaults with explicit worst-case latency in the docs.
Location
kalshi/_base_client.py:77-87—_compute_backoffkalshi/_base_client.py:130-200(sync) and:262-330(async) — retry loops have no elapsed-time checkkalshi/config.py:47-50— defaultsEvidence
Worst-case computation for a single call:
Recommended fix
Add
total_timeout: float | None = None(orrequest_deadline) toKalshiConfig. In the retry loop, checktime.monotonic() - start > total_timeoutbefore each sleep AND before each new attempt, short-circuiting with the last error.Reasonable defaults:
total_timeout = 60.0so the absolute worst case is boundedretry_max_delaydefault to ~5s (gives ~45s worst case)Document the budget calculation in
docs/configuration.mdso operators can pick saner values for their workload.Add tests:
test_total_timeout_short_circuits_retries— mocktime.monotonicto exceed budget after 2 attempts; assert the 3rd never fires.test_total_timeout_None_preserves_legacy_behavior— unchanged retry count whentotal_timeout=None.Severity & category
high / correctness