In the enterprise AI landscape of 2026, Large Language Models (LLMs) are no longer just a novelty; they are critical infrastructure. However, LLM inference is inherently expensive and computationally heavy. When you expose a multi-agent Retrieval-Augmented Generation (RAG) system via an API, you open the door to abuse, budget-draining prompt injection loops, and the "noisy neighbor" problem. If a single client spams your RAG endpoint, they can exhaust your LLM token quotas, spike your cloud compute costs, and degrade latency for all other users. This article provides an end-to-end guide to implementing a Dual-Layer Rate Limiting Middleware in FastAPI. We will enforce limits based on both Client IP (for anonymous/internal traffic) and API Key (for authenticated partners), returning standard 429 Too Many Requests responses. Finally, we will integrate this middleware with a production-grade Multi-Agent LangGraph RAG system featuring persistent memory and state.
Part 1: The Enterprise Use Case
Scenario: "Acme Corp" has deployed an internal/external AI Knowledge Assistant. The system uses a multi-agent LangGraph architecture to retrieve data from Confluence, Jira, and internal wikis, and synthesize answers.
The Access Tiers:
Authenticated Partners (API Key): Have a strict limit (e.g., 60 requests per minute) to prevent automated script abuse.
Internal Employees (Corporate IP): Have a higher limit (e.g., 120 requests per minute) as they are trusted internal users.
Anonymous Web Chat (Public IP): Have the lowest limit (e.g., 20 requests per minute) to prevent bot attacks.
The Architecture:
FastAPI Gateway: Handles HTTP routing, authentication extraction, and rate limiting.
Redis: Acts as the distributed, high-performance counter for rate limits.
LangGraph Backend: Executes the multi-agent RAG workflow, maintaining conversation state via a Checkpointer.
Part 2: Designing the Dual-Layer Rate Limiter
To build an enterprise-grade rate limiter, we must address three challenges:
Distributed State: If your FastAPI app runs on multiple replicas (Kubernetes pods), in-memory counters will fail. We must use Redis.
Race Conditions: A simple
GETthenSETin Redis is not atomic. We will use RedisINCRwith an expiration to ensure thread safety.Header Standards: When rejecting a request with a
429, we must return standard headers (X-RateLimit-Limit,X-RateLimit-Remaining,Retry-After) so client applications can handle the backoff gracefully.

Part 3: The Code Implementation
Below is the complete, end-to-end implementation.
1. Dependencies
pip install fastapi uvicorn redis langchain-openai langgraph langchain-core pydantic2. The Redis Rate Limiter & FastAPI Middleware
import time
import logging
from typing import Optional, Tuple
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
import redis.asyncio as redis
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Redis connection (Enterprise: Use a connection pool)
redis_client = redis.from_url("redis://localhost:6379", decode_responses=True)
class DualLayerRateLimiter:
"""Handles the logic for checking and incrementing rate limits in Redis."""
def __init__(self, redis_conn: redis.Redis):
self.redis = redis_conn
async def check_limit(self, identifier: str, limit: int, window_seconds: int) -> Tuple[bool, int, int]:
"""
Checks the rate limit. Uses a sliding/fixed window approach.
Returns: (is_allowed, remaining_requests, limit)
"""
# Create a time-bounded key (Fixed window per minute)
current_window = int(time.time() // window_seconds)
redis_key = f"ratelimit:{identifier}:{current_window}"
# Atomic increment. If key doesn't exist, it's created with value 1.
current_count = await self.redis.incr(redis_key)
# Set expiration only on the first request of the window to prevent race conditions
if current_count == 1:
await self.redis.expire(redis_key, window_seconds + 1) # +1s buffer
is_allowed = current_count <= limit
remaining = max(0, limit - current_count)
return is_allowed, remaining, limit
class RateLimitMiddleware(BaseHTTPMiddleware):
"""
FastAPI Middleware that intercepts requests, extracts IP/API Key,
and enforces dual-layer rate limiting.
"""
def __init__(self, app, limiter: DualLayerRateLimiter):
super().__init__(app)
self.limiter = limiter
# Define tiers: (limit per minute)
self.api_key_limit = 60
self.internal_ip_limit = 120
self.public_ip_limit = 20
self.window = 60 # 60 seconds
async def dispatch(self, request: Request, call_next):
# Skip rate limiting for health checks and docs
if request.url.path in ["/health", "/docs", "/openapi.json"]:
return await call_next(request)
# 1. Extract Client IP (Handling reverse proxies like AWS ALB / Nginx)
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
client_ip = forwarded_for.split(",")[0].strip()
else:
client_ip = request.client.host if request.client else "unknown"
# 2. Extract API Key
api_key = request.headers.get("X-API-Key") or request.headers.get("Authorization", "").replace("Bearer ", "")
# 3. Determine Identifier and Limit Tier
if api_key and api_key != "":
identifier = f"key:{api_key}"
limit = self.api_key_limit
tier = "API_KEY"
elif client_ip.startswith("10.") or client_ip.startswith("192.168."): # Mock internal IP check
identifier = f"ip:{client_ip}"
limit = self.internal_ip_limit
tier = "INTERNAL_IP"
else:
identifier = f"ip:{client_ip}"
limit = self.public_ip_limit
tier = "PUBLIC_IP"
# 4. Check Limit
is_allowed, remaining, max_limit = await self.limiter.check_limit(identifier, limit, self.window)
if not is_allowed:
logger.warning(f"Rate limit exceeded for {tier} {identifier}. Limit: {max_limit}")
return JSONResponse(
status_code=429,
content={"error": "Too Many Requests", "message": "Rate limit exceeded. Please slow down."},
headers={
"X-RateLimit-Limit": str(max_limit),
"X-RateLimit-Remaining": "0",
"Retry-After": str(self.window),
"X-RateLimit-Tier": tier
}
)
# 5. Proceed and inject rate limit headers into the successful response
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(max_limit)
response.headers["X-RateLimit-Remaining"] = str(remaining)
response.headers["X-RateLimit-Tier"] = tier
return response
Join the conversation! Your thoughts help the community grow.