Skip to content

Retry-After header honored only on 429; 503/504/408 silently ignored #322

Closed

Description

@TexasCoding

Problem

_map_error parses Retry-After only inside the status == 429 branch and attaches the value exclusively to KalshiRateLimitError. For 503 (Kalshi's own scheduled-maintenance pattern, AWS ALB throttles, Cloudflare "Under Attack" mode), 504, and 408 — all retryable status codes — the header is silently ignored, and the retry loop falls back to _compute_backoff capped at retry_max_delay (default 30 s). A 503 with Retry-After: 60 is therefore re-hammered every couple of seconds while the upstream is asking for a full minute.

RFC 7231 §7.1.3 explicitly defines Retry-After as the canonical signal for 503 and permits it on any retryable 4xx. The parsing logic at _base_client.py:93-129 is otherwise correct (battle-tested by #267) — it just isn't reachable from the 5xx/timeout path.

Evidence

  • kalshi/_base_client.py:92-132 — header only parsed in the 429 branch.
  • kalshi/_base_client.py:137-140 — 408/504 → KalshiTimeoutError with no retry_after; 5xx → KalshiServerError, same.
  • kalshi/_base_client.py:391-394 & 601-604 — retry loop only consults retry_after on KalshiRateLimitError
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/errors.py:59 — retry_after defined only on KalshiRateLimitError.

Suggested fix

Extract a _parse_retry_after(response) -> float | None helper from the 429 branch and call it from every retryable error path. Expose retry_after: float | None on the shared KalshiError base (or at least on KalshiServerError and KalshiTimeoutError). The retry loop becomes one line:

retry_after = getattr(error, "retry_after", None)
delay = min(retry_after, self._config.retry_max_delay) if retry_after is not None else _compute_backoff(...)

Add a regression test mirroring test_429_retry_after_caps_at_retry_max_delay for status 503.

Source

Round-3 independent audit (reviewer: http_transport).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingperformancePerformance / hot-path concern

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions