virattt/ai-hedge-fund · error · FDClientError

{method} {path} rate limited (429) after {len(self._RETRY_DE

Error message

{method} {path} rate limited (429) after {len(self._RETRY_DELAYS)} retries

What it means

Raised by FDClient._request (hedge_fund/data/client.py:300) after the client exhausted its retry schedule for HTTP 429 responses. The delays are (5, 15, 30) seconds — three retries — after which the loop falls through and raises FDClientError with status_code=429. This means the caller is being rate-limited harder than the built-in backoff can absorb.

Source

Thrown at hedge_fund/data/client.py:300

                logger.info(
                    "Rate limited (429), retrying in %ds (attempt %d/%d)",
                    delay, attempt + 1, len(self._RETRY_DELAYS),
                )
                time.sleep(delay)
                continue

            if resp.status_code == 404:
                return None

            if resp.status_code >= 400:
                raise FDClientError(
                    f"{method} {path} returned {resp.status_code}: {resp.text[:200]}",
                    status_code=resp.status_code, path=path,
                )

            return resp

        raise FDClientError(
            f"{method} {path} rate limited (429) after {len(self._RETRY_DELAYS)} retries",
            status_code=429, path=path,
        )

View on GitHub (pinned to eff8a7320f)

Solutions

  1. Slow down or serialize request volume: reduce universe size, cache more aggressively (CachedDataClient), or add spacing between cycles.
  2. Upgrade the API plan or use a key with a higher rate limit, then re-run.
  3. Catch FDClientError with status_code == 429 and re-run the whole backtest after a longer sleep (e.g. 60–120s) — the internal 5/15/30s backoff was insufficient.
  4. Run only one backtest at a time per API key; kill duplicate processes sharing the key.

Example fix

# before
result = run_backtest(fund, FDClient(), start, end)  # dies mid-run on 429 after 3 retries

# after
import time
from hedge_fund.data.client import FDClient, FDClientError

for attempt in range(5):
    try:
        result = run_backtest(fund, FDClient(), start, end)
        break
    except FDClientError as e:
        if e.status_code != 429 or attempt == 4:
            raise
        time.sleep(120)
Defensive patterns

Strategy: retry

Validate before calling

def under_rate_pressure(client) -> bool:
    """Detect sustained 429s early: probe once before the big run."""
    try:
        client.get_prices("SPY", "2024-01-02", "2024-01-03")
        return False
    except Exception as e:
        return getattr(e, "status_code", None) == 429

Type guard

from hedge_fund.data.client import FDClientError

def is_rate_limited(e: BaseException) -> bool:
    return isinstance(e, FDClientError) and e.status_code == 429

Try / catch

from hedge_fund.data.client import FDClient, FDClientError
import time

for attempt in range(5):
    try:
        result = run_backtest(fund, FDClient(), start, end, universe)
        break
    except FDClientError as e:
        if e.status_code != 429 or attempt == 4:
            raise
        time.sleep(120 * (attempt + 1))  # longer than the client's 5/15/30s

Prevention

When it happens

Trigger: Any FDClient API call while the provider is persistently returning 429: a free-tier key with a very low requests/minute cap hit by a large-universe backtest (one request per ticker per cycle); parallel runs sharing one key; running a warm-up pass plus a backtest simultaneously (as the TUI worker does).

Common situations: Free FMP tier (~a few calls/min) with a 50-ticker universe; multiple developers/processes sharing one key; end-of-day when everyone hits the API; switching from a paid plan to free without shrinking the request volume.

Related errors


AI-assisted analysis of virattt/ai-hedge-fund@eff8a7320f (2026-08-15). Data as JSON: /api/errors/436e83e9d665e3c5. Report an issue: GitHub.