{"record":{"id":"436e83e9d665e3c5","repo":"virattt/ai-hedge-fund","slug":"method-path-rate-limited-429-after-len-self","errorCode":null,"errorMessage":"{method} {path} rate limited (429) after {len(self._RETRY_DELAYS)} retries","messagePattern":"(.+?) (.+?) rate limited \\(429\\) after (.+?) retries","errorType":"exception","errorClass":"FDClientError","httpStatus":null,"severity":"error","filePath":"hedge_fund/data/client.py","lineNumber":300,"sourceCode":"                logger.info(\n                    \"Rate limited (429), retrying in %ds (attempt %d/%d)\",\n                    delay, attempt + 1, len(self._RETRY_DELAYS),\n                )\n                time.sleep(delay)\n                continue\n\n            if resp.status_code == 404:\n                return None\n\n            if resp.status_code >= 400:\n                raise FDClientError(\n                    f\"{method} {path} returned {resp.status_code}: {resp.text[:200]}\",\n                    status_code=resp.status_code, path=path,\n                )\n\n            return resp\n\n        raise FDClientError(\n            f\"{method} {path} rate limited (429) after {len(self._RETRY_DELAYS)} retries\",\n            status_code=429, path=path,\n        )\n","sourceCodeStart":282,"sourceCodeEnd":304,"githubUrl":"https://github.com/virattt/ai-hedge-fund/blob/eff8a7320fcf0b473b135690fa1a5b0d9b022a83/hedge_fund/data/client.py#L282-L304","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"# before\nresult = run_backtest(fund, FDClient(), start, end)  # dies mid-run on 429 after 3 retries\n\n# after\nimport time\nfrom hedge_fund.data.client import FDClient, FDClientError\n\nfor attempt in range(5):\n    try:\n        result = run_backtest(fund, FDClient(), start, end)\n        break\n    except FDClientError as e:\n        if e.status_code != 429 or attempt == 4:\n            raise\n        time.sleep(120)","handlingStrategy":"retry","validationCode":"def under_rate_pressure(client) -> bool:\n    \"\"\"Detect sustained 429s early: probe once before the big run.\"\"\"\n    try:\n        client.get_prices(\"SPY\", \"2024-01-02\", \"2024-01-03\")\n        return False\n    except Exception as e:\n        return getattr(e, \"status_code\", None) == 429","typeGuard":"from hedge_fund.data.client import FDClientError\n\ndef is_rate_limited(e: BaseException) -> bool:\n    return isinstance(e, FDClientError) and e.status_code == 429","tryCatchPattern":"from hedge_fund.data.client import FDClient, FDClientError\nimport time\n\nfor attempt in range(5):\n    try:\n        result = run_backtest(fund, FDClient(), start, end, universe)\n        break\n    except FDClientError as e:\n        if e.status_code != 429 or attempt == 4:\n            raise\n        time.sleep(120 * (attempt + 1))  # longer than the client's 5/15/30s","preventionTips":["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."],"tags":["rate-limit","http","data-client","retry"],"backgroundTag":null,"analyzedSha":"eff8a7320fcf0b473b135690fa1a5b0d9b022a83","analyzedAt":"2026-08-15T00:22:46.567Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}