PrefectHQ/fastmcp · warning · ToolError
Rate limited by upstream API, please retry later
Error message
Rate limited by upstream API, please retry later
What it means
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.
Source
Thrown at fastmcp_slim/fastmcp/server/server.py:1545
# it says the request cannot be serviced at all, and
# SEP-2575 requires it on the wire as -32021 (HTTP 400).
# Flattening it into a result would drop the code and tell
# the client the call had succeeded.
if (
isinstance(e, MCPError)
and e.error.code == MISSING_REQUIRED_CLIENT_CAPABILITY
):
logger.debug(
"Tool %r requires a client capability the client did "
"not declare",
name,
)
raise
logger.exception(f"Error calling tool {name!r}")
# Handle actionable errors that should reach the LLM
# even when masking is enabled
if get_http_status_code(e) == 429:
raise ToolError(
"Rate limited by upstream API, please retry later"
) from e
if is_timeout_error(e):
raise ToolError(
"Upstream request timed out, please retry"
) from e
# Standard masking logic
if self._mask_error_details:
raise ToolError(f"Error calling tool {name!r}") from e
raise ToolError(f"Error calling tool {name!r}: {e}") from e
async def read_resource(
self,
uri: str,
*,
version: VersionSpec | None = None,
run_middleware: bool = True,
) -> ResourceResult:View on GitHub (pinned to 1f02114297)
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
Example fix
// before: tight retry loop
for _ in range(10):
await client.call_tool('search_web', {'q': '...'})
// after: backoff on the rate-limit ToolError
import asyncio
for attempt in range(5):
try:
return await client.call_tool('search_web', {'q': '...'})
except ToolError as e:
if 'Rate limited' not in str(e):
raise
await asyncio.sleep(2 ** attempt) Defensive patterns
Strategy: retry
Type guard
def is_rate_limited(e: BaseException) -> bool:
return isinstance(e, ToolError) and 'Rate limited' in str(e) Try / catch
import asyncio
for attempt in range(5):
try:
return await client.call_tool('my_tool', args)
except ToolError as e:
if 'Rate limited' not in str(e):
raise
await asyncio.sleep(min(2 ** attempt, 60))
raise RuntimeError('still rate limited after retries') Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Upstream request timed out, please retry
- Tool call limit exceeded: at most {max_tool_calls} call_tool
- OIDC discovery for issuer {issuer!r} recently failed; backin
- Authorization failed for tool '{tool_name}': not found or no
- Ambiguous app tool {tool_name!r}: {len(matches)} components
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/c56180aa5d5397e0.
Report an issue: GitHub.