{"record":{"id":"c56180aa5d5397e0","repo":"PrefectHQ/fastmcp","slug":"rate-limited-by-upstream-api-please-retry-later","errorCode":null,"errorMessage":"Rate limited by upstream API, please retry later","messagePattern":"Rate limited by upstream API, please retry later","errorType":"exception","errorClass":"ToolError","httpStatus":429,"severity":"warning","filePath":"fastmcp_slim/fastmcp/server/server.py","lineNumber":1545,"sourceCode":"                    # it says the request cannot be serviced at all, and\n                    # SEP-2575 requires it on the wire as -32021 (HTTP 400).\n                    # Flattening it into a result would drop the code and tell\n                    # the client the call had succeeded.\n                    if (\n                        isinstance(e, MCPError)\n                        and e.error.code == MISSING_REQUIRED_CLIENT_CAPABILITY\n                    ):\n                        logger.debug(\n                            \"Tool %r requires a client capability the client did \"\n                            \"not declare\",\n                            name,\n                        )\n                        raise\n                    logger.exception(f\"Error calling tool {name!r}\")\n                    # Handle actionable errors that should reach the LLM\n                    # even when masking is enabled\n                    if get_http_status_code(e) == 429:\n                        raise ToolError(\n                            \"Rate limited by upstream API, please retry later\"\n                        ) from e\n                    if is_timeout_error(e):\n                        raise ToolError(\n                            \"Upstream request timed out, please retry\"\n                        ) from e\n                    # Standard masking logic\n                    if self._mask_error_details:\n                        raise ToolError(f\"Error calling tool {name!r}\") from e\n                    raise ToolError(f\"Error calling tool {name!r}: {e}\") from e\n\n    async def read_resource(\n        self,\n        uri: str,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n    ) -> ResourceResult:","sourceCodeStart":1527,"sourceCodeEnd":1563,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/server.py#L1527-L1563","documentation":"In call_tool's generic exception handler, any unexpected exception from tool._run whose HTTP status code is 429 is converted to ToolError('Rate limited by upstream API, please retry later'). FastMCP recognizes rate-limit errors as actionable even when error masking is enabled, so the LLM/client learns it should back off and retry.","triggerScenarios":"A tool body calling an upstream HTTP API that returns 429 (or raises an exception carrying status 429, detected via get_http_status_code) — the wrapper tool call then surfaces this ToolError.","commonSituations":"Tools proxying third-party APIs (OpenAI, GitHub, etc.) under burst load; shared API keys exhausting org-wide quotas; missing/incorrect API keys sometimes surfacing as 429 from gateways; retry loops with no backoff hammering a rate-limited endpoint.","solutions":["Retry the call after a delay with exponential backoff (the error explicitly invites retry later)","Reduce call frequency or batch requests to the upstream API","Use a higher-quota API key/tier or rotate across keys","Cache upstream responses in the tool to cut request volume","Inspect the chained exception (the 429 cause) to see which upstream limit was hit"],"exampleFix":"// before: tight retry loop\nfor _ in range(10):\n    await client.call_tool('search_web', {'q': '...'})\n// after: backoff on the rate-limit ToolError\nimport asyncio\nfor attempt in range(5):\n    try:\n        return await client.call_tool('search_web', {'q': '...'})\n    except ToolError as e:\n        if 'Rate limited' not in str(e):\n            raise\n        await asyncio.sleep(2 ** attempt)","handlingStrategy":"retry","validationCode":null,"typeGuard":"def is_rate_limited(e: BaseException) -> bool:\n    return isinstance(e, ToolError) and 'Rate limited' in str(e)","tryCatchPattern":"import asyncio\nfor attempt in range(5):\n    try:\n        return await client.call_tool('my_tool', args)\n    except ToolError as e:\n        if 'Rate limited' not in str(e):\n            raise\n        await asyncio.sleep(min(2 ** attempt, 60))\nraise RuntimeError('still rate limited after retries')","preventionTips":["Always retry 429-style ToolErrors with exponential backoff and jitter","Add client-side rate limiting before hitting quota-sensitive tools","Cache upstream responses inside tools","Monitor quota usage and upgrade API tiers before limits are hit"],"tags":["rate-limit","http-429","tools","retry"],"backgroundTag":"rate-limit-429","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}