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
- Slow down or serialize request volume: reduce universe size, cache more aggressively (CachedDataClient), or add spacing between cycles.
- Upgrade the API plan or use a key with a higher rate limit, then re-run.
- 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.
- 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
- Wrap runs with one outer retry on status_code == 429 — the client's internal backoff (5/15/30s) may be shorter than the provider's window.
- Use CachedDataClient so repeated backtests don't re-fetch the same bars.
- One API key per concurrent run; don't share keys across processes.
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
- {method} {path} failed: {exc}
- {method} {path} returned {resp.status_code}: {resp.text[:200
- no JSON object found in response: {text[:200]!r}
AI-assisted analysis of virattt/ai-hedge-fund@eff8a7320f (2026-08-15).
Data as JSON: /api/errors/436e83e9d665e3c5.
Report an issue: GitHub.