PrefectHQ/fastmcp · warning · ToolError

Upstream request timed out, please retry

Error message

Upstream request timed out, please retry

What it means

call_tool's exception handler converts any tool exception matching is_timeout_error into ToolError('Upstream request timed out, please retry'). Like the 429 case, timeouts are treated as actionable and reach the client even when _mask_error_details is on, signaling a transient failure worth retrying.

Solutions

  1. Retry the tool call — the error is transient by design
  2. Increase the timeout in the tool's internal HTTP/DB client configuration
  3. Add circuit-breaking/deadline budgets inside the tool and partial results on timeout
  4. Check upstream service health/status pages
  5. Move long work behind an async job pattern instead of blocking the tool call

Example fix

// inside the tool: before
def fetch():
    return httpx.get(url)
// after
def fetch():
    return httpx.get(url, timeout=httpx.Timeout(30.0))
Defensive patterns

Strategy: retry

Type guard

def is_timeout(e: BaseException) -> bool:
    return isinstance(e, ToolError) and 'timed out' in str(e)

Try / catch

import asyncio
for attempt in range(3):
    try:
        return await client.call_tool('my_tool', args)
    except ToolError as e:
        if 'timed out' not in str(e):
            raise
        await asyncio.sleep(2 ** attempt)
raise RuntimeError('tool kept timing out')

Prevention

When it happens

Trigger: A tool's handler makes an upstream network call (HTTP, DB, socket) that times out — httpx.TimeoutException, asyncio.TimeoutError, socket.timeout, etc. — and the exception escapes tool._run.

Common situations: Upstream API slow or degraded; too-short client timeouts in the tool's HTTP client; large payloads/slow queries; network partitions between the MCP server and its dependencies.

Understand the failure class

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/e6c7166ecfaa73cc. Report an issue: GitHub.

Appendix: source

Thrown at fastmcp_slim/fastmcp/server/server.py:1549

                    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:
        """Read a resource by URI.

        This is the public API for reading resources. By default, middleware is applied.
        Checks concrete resources first, then templates.

View on GitHub (pinned to 1f02114297)