In a Python-based backend using FastAPI and FastMCP, HTTP requests were handled with two distinct rate-limiting mechanisms: an in-memory middleware approach and a Redis-backed solution. The middleware rate limiting failed due to the architecture’s reliance on subprocesses, where each request spawned a new subprocess with its own isolated memory state.
Issue
The FastMCP middleware rate limiting mechanism was ineffective because each request spawned a new subprocess, each with its own in-memory counter. As a result, the middleware’s configured limit (e.g., 50 requests per second) was never enforced, as each subprocess only processed a single request before terminating. This design flaw meant the rate limiting was reset for every new request, rendering it ineffective.
Culprit
The subprocess-per-request architecture caused the following behavior:
- Each request initiated a new subprocess, which reset the rate-limiting counter to zero.
- The in-memory counter in the middleware was not shared across subprocesses, making it impossible to track requests across multiple invocations.
- Unlike the Redis-backed rate limiting, which persists across all API instances, the middleware relied on ephemeral in-memory storage that was lost when the subprocess exited.
Solution
Option A: Retain Middleware as a Safety Net (Implemented)
The middleware rate limiting was kept as a lightweight safeguard with minimal performance overhead. Although ineffective for cross-request rate limiting, it provides:
- Protection against recursive tool calls within a single request.
- Flexibility for future architectural changes, such as transitioning to a long-running HTTP server model.
Option B: Remove Middleware for Simplicity (Rejected)
While this would simplify the architecture, it would eliminate the marginal safety net provided by the middleware. The tradeoff was deemed unnecessary given the existence of the more robust Redis-backed solution.
Effective Rate Limiting via Redis
The primary rate limiting was handled by a Redis-backed layer, which ensured consistent enforcement across all API pods and subprocesses. This approach:
- Uses shared state across all API instances, allowing requests to be tracked globally.
- Implements a configurable limit (e.g., 60 requests per minute) enforced at the HTTP layer.
- Provides reliable cross-request rate limiting, unaffected by subprocess isolation.
This design ensured that rate limiting was effective and persistent, addressing the core issue while maintaining flexibility for future adjustments.