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:
-
Define Policy Constants – Set
REQUESTS_PER_MINUTE = 100and choose between per-user or per-tool limits based on API costs and authentication tiers. -
Configure Redis Bucket – Use keys formatted as
rate:{user_id}:{tool_name}with atomicINCRandEXPIRE(orPEXPIRE) operations to maintain the token bucket and enable automatic window rotation. -
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.
-
Standardize Error Responses – Return MCP error objects with
isError: trueand descriptive 429-style messages indicating the specific limit exceeded and retry guidance. -
Annotate Tool Metadata – Add
rateLimit: "100 req/min"to toolannotationsfields so LLM clients discover quotas before invocation and can plan request patterns. -
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:
mcp-builder/reference/mcp_best_practices.md– Contains explicit guidance to "Consider rate limiting for resource-intensive operations" (line 42) and security logging recommendations (line 44).mcp-builder/SKILL.md– Lists rate limiting and pagination patterns as core implementation concerns for production MCP servers.notion-spec-to-implementation/examples/api-feature.md– Demonstrates concrete rate limits of 100 requests per minute per user and their inclusion in generated implementation plans (line 69).
Summary
- Token-bucket architecture: Use Redis with atomic
INCRand 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: truewith descriptive 429 messages when rate limits are exceeded, allowing LLMs to implement backoff and retry logic. - Metadata exposure: Include
rateLimitannotations 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
What is the recommended default rate limit for MCP tools?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →