How to Handle Rate Limiting in MCP Implementations: A Complete Guide

Implement token-bucket rate limiting using Redis to enforce per-user or per-tool quotas (such as 100 requests per minute), wrap MCP tool calls in decorator or middleware guards that return standardized error responses when limits are exceeded, and expose rate limit metadata in tool annotations so LLMs can plan requests accordingly.

Handling rate limiting in MCP implementations is essential to prevent individual clients from overwhelming external APIs and exhausting server resources. The ComposioHQ/awesome-codex-skills repository provides authoritative architectural patterns demonstrating how to safeguard Model Context Protocol servers using Redis-backed token buckets and standardized MCP error responses. According to the MCP best practices guide and concrete API examples in the repository, developers should enforce strict quotas while maintaining clear communication channels with LLM clients about throughput constraints.

Architectural Approach to Rate Limiting

The ComposioHQ/awesome-codex-skills repository outlines a five-step architectural pattern for handling rate limiting in MCP implementations. As explicitly stated in mcp-builder/reference/mcp_best_practices.md (line 42), developers should "Consider rate limiting for resource-intensive operations" and "Rate limit requests" to prevent service abuse and protect downstream resources.

Define Rate Limit Policies Per Tool or User

Establish token-bucket policies based on authentication status and external API costs. The repository recommends a concrete quota of 100 requests per minute per user for standard operations, as illustrated in notion-spec-to-implementation/examples/api-feature.md (line 69). Resource-intensive external APIs may require stricter limits (e.g., 10 req/min), while high-throughput internal tools can sustain more permissive thresholds.

Persist Counters in Redis

Use Redis as the rate-limiting data store to leverage atomic increment operations and automatic TTL expiration. Store counters in keys formatted as rate:{user_id}:{tool_name} or rate:{user_id}:{window_timestamp}. This approach ensures the token bucket refills automatically when the time window expires, eliminating the need for explicit cleanup tasks and preventing race conditions through atomic INCR operations.

Wrap Tool Implementations with Rate-Limit Guards

Implement guards as Python decorators or TypeScript middleware that intercept tool calls before execution. These guards check the current bucket count using redis.incr(); if the limit is exceeded, they immediately return an MCP error response with isError: true without invoking the external API, preventing unnecessary resource consumption and costly out-of-quota calls.

Expose Limits in Tool Metadata

Include a rateLimit field in tool annotations to communicate quotas to LLM clients. As implemented in the repository examples, this metadata allows language models to understand expected throughput before invocation, enabling intelligent request batching and backoff strategies that reduce unintentional throttling and improve user experience.

Log and Monitor Denials

Record every rate-limit hit and denial to detect abuse patterns and tune policies dynamically. The best practices guide in mcp-builder/reference/mcp_best_practices.md (line 44) specifically recommends logging security events, including rate limit violations, to maintain observability over client behavior and identify potential misuse patterns.

Code Implementation Examples

The following examples demonstrate token-bucket rate limiting using Redis in Python and TypeScript MCP servers.

Python FastMCP Implementation

In FastMCP implementations, implement rate limiting as a decorator that wraps tool functions. The decorator checks a Redis counter keyed to the user and current time window, returning a standard CallToolResult with isError: true when the bucket empties:

import time
import aioredis
from mcp.server.fastmcp import FastMCP
from mcp.types import CallToolResult, TextContent

redis = aioredis.from_url("redis://localhost")
REQUESTS_PER_MINUTE = 100
WINDOW_SECONDS = 60

def rate_limit(key: str):
    async def wrapper(func):
        async def inner(*args, **kwargs):
            now = int(time.time())
            bucket_key = f"rate:{key}:{now // WINDOW_SECONDS}"
            count = await redis.incr(bucket_key)
            if count == 1:
                # first hit, set TTL for the window

                await redis.expire(bucket_key, WINDOW_SECONDS)
            if count > REQUESTS_PER_MINUTE:
                return CallToolResult(
                    isError=True,
                    content=[TextContent(type="text", text="Rate limit exceeded (100 req/min).")]
                )
            return await func(*args, **kwargs)
        return inner
    return wrapper

mcp = FastMCP("example_mcp")

@mcp.tool(
    annotations={
        "title": "Fetch Weather",
        "readOnlyHint": True,
        "openWorldHint": True,
        "rateLimit": "100 req/min"
    }
)
@rate_limit("weather")
async def fetch_weather(location: str) -> str:
    """Call an external weather API."""
    # ... perform HTTP request ...

    return "Sunny, 23°C"

TypeScript MCP SDK Implementation

For TypeScript servers using the MCP SDK, implement rate limiting as middleware that executes before the tool handler. The middleware increments a Redis counter and returns an error object compatible with the MCP protocol when limits are exceeded:

import { Server } from "@modelcontextprotocol/sdk";
import Redis from "ioredis";

const redis = new Redis();
const REQUESTS_PER_MINUTE = 100;
const WINDOW_MS = 60_000;

function rateLimit(toolName: string) {
  return async (ctx: any, next: () => Promise<any>) => {
    const user = ctx.auth?.userId ?? "anonymous";
    const bucketKey = `rate:${user}:${toolName}`;
    const count = await redis.incr(bucketKey);
    if (count === 1) {
      await redis.pexpire(bucketKey, WINDOW_MS);
    }
    if (count > REQUESTS_PER_MINUTE) {
      return {
        isError: true,
        content: [{ type: "text", text: "Rate limit exceeded (100 req/min)." }],
      };
    }
    return next();
  };
}

const server = new Server({ name: "example-mcp" });

server.registerTool({
  name: "search_web",
  description: "Perform a web search.",
  inputSchema: {
    type: "object",
    properties: { query: { type: "string" } },
    required: ["query"],
  },
  annotations: {
    title: "Web Search",
    readOnlyHint: true,
    openWorldHint: true,
    rateLimit: "100 req/min",
  },
  handler: async (params) => {
    // actual search logic
    return { content: [{ type: "text", text: "Results…" }] };
  },
  middleware: [rateLimit("search_web")],
});

Implementation Checklist

Follow these steps to implement rate limiting in your MCP server according to the ComposioHQ/awesome-codex-skills patterns:

  1. Define Policy Constants – Set REQUESTS_PER_MINUTE = 100 and choose between per-user or per-tool limits based on API costs and authentication tiers.

  2. Configure Redis Bucket – Use keys formatted as rate:{user_id}:{tool_name} with atomic INCR and EXPIRE (or PEXPIRE) operations to maintain the token bucket and enable automatic window rotation.

  3. Create Guard Decorator/Middleware – Implement the guard to check the bucket, increment counters, and short-circuit execution when counters exceed limits before calling external APIs.

  4. Standardize Error Responses – Return MCP error objects with isError: true and descriptive 429-style messages indicating the specific limit exceeded and retry guidance.

  5. Annotate Tool Metadata – Add rateLimit: "100 req/min" to tool annotations fields so LLM clients discover quotas before invocation and can plan request patterns.

  6. Enable Structured Logging – Emit logs for every rate-limit hit and denial as recommended in mcp-builder/reference/mcp_best_practices.md (line 44), including user identifiers and tool names for abuse analysis.

Key Source References

The architectural patterns above derive from specific authoritative files in the repository:

Summary

  • Token-bucket architecture: Use Redis with atomic INCR and TTLs to enforce 100 requests per minute per user quotas without manual cleanup or race conditions.
  • Guard pattern: Wrap MCP tool calls in Python decorators or TypeScript middleware to validate limits before executing external API requests.
  • Standard errors: Return isError: true with descriptive 429 messages when rate limits are exceeded, allowing LLMs to implement backoff and retry logic.
  • Metadata exposure: Include rateLimit annotations in tool definitions so clients can plan request patterns and avoid unnecessary throttling.
  • Observability: Log all rate-limit denials as security events to detect abuse patterns and dynamically tune policies based on actual usage.

Frequently Asked Questions

The ComposioHQ/awesome-codex-skills repository suggests a default of 100 requests per minute per user for standard MCP tools, as demonstrated in notion-spec-to-implementation/examples/api-feature.md (line 69). High-cost external APIs may require stricter limits, while internal read-only operations may tolerate higher throughput based on infrastructure capacity.

Should I implement rate limiting per user or per tool?

Implement per-user rate limiting for multi-tenant servers to prevent individual clients from exhausting shared quotas, and per-tool limits for resource-intensive operations that could overwhelm external APIs regardless of the requesting user. The best practices guide recommends evaluating both dimensions based on API costs, authentication status, and observed abuse patterns.

How should an MCP server respond when a rate limit is exceeded?

Return a standard MCP error response with isError: true and a content array containing a text explanation of the 429-style error, such as "Rate limit exceeded (100 req/min)." This signals to the LLM that it should back off and retry later, rather than treating the response as successful tool output or attempting immediate retries that would compound the issue.

Where should rate limit counters be stored?

Use Redis as the persistent store for rate limit counters because it supports atomic INCR operations and automatic TTL expiration via EXPIRE or PEXPIRE. This ensures the token bucket refills correctly without race conditions, manual cleanup tasks, or database overhead, making it ideal for high-throughput MCP implementations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →