PrefectHQ/fastmcp · error · RateLimitError

Rate limit exceeded: {self.max_requests} requests per {self.

Error message

Rate limit exceeded: {self.max_requests} requests per {self.window_seconds // 60} minutes for client: {client_id}

What it means

Raised as RateLimitError by SlidingWindowRateLimitingMiddleware.on_request when the client's sliding-window count shows it already made max_requests within window_seconds. Unlike the token-bucket middleware, the message echoes the configured limit and window in minutes.

Source

Thrown at fastmcp_slim/fastmcp/server/middleware/rate_limiting.py:238

        )

    async def _get_client_identifier(self, context: MiddlewareContext) -> str:
        """Get client identifier for rate limiting."""
        if self.get_client_id:
            client_id = self.get_client_id(context)
            if inspect.isawaitable(client_id):
                return cast(str, await client_id)
            return client_id
        return "global"

    async def on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any:
        """Apply sliding window rate limiting to requests."""
        client_id = await self._get_client_identifier(context)
        limiter = self.limiters[client_id]

        allowed = await limiter.is_allowed()
        if not allowed:
            raise RateLimitError(
                f"Rate limit exceeded: {self.max_requests} requests per "
                f"{self.window_seconds // 60} minutes for client: {client_id}"
            )

        return await call_next(context)

View on GitHub (pinned to 1f02114297)

Solutions

  1. Pace client requests to stay under max_requests / window_seconds.
  2. Increase max_requests or window_seconds to fit legitimate workload.
  3. Catch RateLimitError and delay until the sliding window frees capacity before retrying.

Example fix

// before
for doc in docs:
    await client.call_tool("index", {"doc": doc})  # 500 docs, 100/min cap

// after: pace to the limit
import anyio
for doc in docs:
    await client.call_tool("index", {"doc": doc})
    await anyio.sleep(0.7)
Defensive patterns

Strategy: retry

Validate before calling

max_requests = 100
window_seconds = 60
max_rps = max_requests / window_seconds  # pace client below this
assert client_rps <= max_rps, "client pacing exceeds server sliding-window limit"

Try / catch

import anyio
try:
    result = await client.call_tool(name, args)
except Exception as e:
    if "Rate limit exceeded" in str(e):
        # wait out the window before retrying
        await anyio.sleep(window_seconds)
        result = await client.call_tool(name, args)
    else:
        raise

Prevention

When it happens

Trigger: Any request arriving when limiter.is_allowed() is False: the calling client_id has hit max_requests within the trailing window_seconds (note the message divides by 60, so windows that are not whole minutes display a floored minute count).

Common situations: Batch jobs exceeding e.g. '100 requests per minute'; window_seconds like 90 displayed as '1 minute' in the message confusing clients about the real window; clients without request pacing.

Related errors


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