# Rate Limiting API rate limiting implementation using sliding window algorithm for request throttling and abuse prevention. ## Overview The platform uses an in-memory rate limiter with a sliding window algorithm to protect endpoints from abuse and ensure fair resource usage across all clients. ## Features - **Sliding Window Algorithm**: Accurate rate limiting based on request timestamps - **Per-Client Tracking**: Individual limits for each client - **Automatic Cleanup**: Removes old entries to prevent memory leaks - **Configurable Limits**: Set custom limits per endpoint - **Decorator-Based**: Easy integration with FastAPI routes ## How It Works ### Sliding Window Algorithm The rate limiter uses a sliding window approach: 1. Records timestamp of each request for a client 2. When new request comes in, removes expired timestamps 3. Counts remaining requests in the current window 4. Allows or denies based on the limit ```mermaid graph LR A[New Request] --> B{Check Window} B --> C[Remove Old Timestamps] C --> D{Count < Limit?} D -->|Yes| E[Allow Request] D -->|No| F[Reject - 429] E --> G[Record Timestamp] ``` ### Example Timeline For a limit of 10 requests per 60 seconds: ``` Time: 0s 10s 20s 30s 40s 50s 60s 70s |-----|-----|-----|-----|-----|-----|-----| Req: 1 2 3 4 5 6 7 8 9 10 <-- Window (60s) --> 11 ^ ALLOWED (req 1-3 expired) ``` ## Configuration ### Global Rate Limiter A global rate limiter instance is available: ```python from middleware.rate_limiter import RateLimiter rate_limiter = RateLimiter() ``` ### Rate Limiter Options | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `cleanup_interval` | int | 3600 | Seconds between automatic cleanups | ## Usage ### Using the Decorator The easiest way to add rate limiting to an endpoint: ```python from middleware.decorators import rate_limit @app.post("/api/v1/resource") @rate_limit(max_requests=10, window_seconds=60) async def create_resource(): return {"status": "created"} ``` ### Decorator Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `max_requests` | int | 100 | Maximum requests allowed | | `window_seconds` | int | 3600 | Time window in seconds (1 hour) | ### Manual Usage For more control, use the rate limiter directly: ```python from middleware.rate_limiter import RateLimiter from app.exceptions import RateLimitException rate_limiter = RateLimiter() @app.post("/api/v1/custom") async def custom_endpoint(request: Request): client_id = request.client.host if not rate_limiter.allow_request(client_id, max_requests=5, window_seconds=60): raise RateLimitException( message="Too many requests", retry_after=60 ) return {"status": "success"} ``` ## Client Identification ### Current Implementation By default, the rate limiter uses a simple client ID: ```python client_id = "anonymous" # Basic implementation ``` ### Production Recommendations For production, implement proper client identification: #### Option 1: By IP Address ```python client_id = request.client.host ``` #### Option 2: By API Key ```python api_key = request.headers.get("X-API-Key", "anonymous") client_id = f"apikey:{api_key}" ``` #### Option 3: By Authenticated User ```python if hasattr(request.state, 'user'): client_id = f"user:{request.state.user.id}" else: client_id = f"ip:{request.client.host}" ``` #### Option 4: Combined Approach ```python def get_client_id(request: Request) -> str: # Prefer authenticated user if hasattr(request.state, 'user'): return f"user:{request.state.user.id}" # Fall back to API key api_key = request.headers.get("X-API-Key") if api_key: return f"key:{api_key}" # Last resort: IP address return f"ip:{request.client.host}" ``` ## Common Rate Limit Configurations ### Conservative Limits For expensive operations or authenticated endpoints: ```python @rate_limit(max_requests=10, window_seconds=3600) # 10 per hour async def expensive_operation(): pass ``` ### Moderate Limits For standard API operations: ```python @rate_limit(max_requests=100, window_seconds=3600) # 100 per hour async def standard_operation(): pass ``` ### Generous Limits For read-heavy operations: ```python @rate_limit(max_requests=1000, window_seconds=3600) # 1000 per hour async def read_operation(): pass ``` ### Per-Minute Limits For real-time operations: ```python @rate_limit(max_requests=60, window_seconds=60) # 60 per minute async def realtime_operation(): pass ``` ## Error Response When rate limit is exceeded, clients receive a 429 status code: ```json { "detail": "Rate limit exceeded", "status_code": 429, "retry_after": 3600, "timestamp": "2024-11-16T13:00:00Z", "path": "/api/v1/resource" } ``` ### Response Headers Consider adding rate limit headers (future enhancement): ```http X-RateLimit-Limit: 100 X-RateLimit-Remaining: 45 X-RateLimit-Reset: 1700145600 ``` ## Memory Management ### Automatic Cleanup The rate limiter automatically cleans up old entries: ```python # Runs every hour by default cleanup_interval = 3600 # seconds ``` ### Manual Cleanup Force cleanup if needed: ```python rate_limiter.cleanup_old_entries() ``` ### Memory Considerations For high-traffic applications: - Each client uses approximately 8 bytes per request timestamp - Example: 1000 clients x 100 requests = approximately 800 KB - Consider Redis for distributed rate limiting ## Advanced Patterns ### Different Limits by Role ```python from fastapi import Depends from models.database.user import User def get_rate_limit_for_user(user: User) -> tuple[int, int]: limits = { "admin": (10000, 3600), # 10k per hour "store": (1000, 3600), # 1k per hour "customer": (100, 3600), # 100 per hour } return limits.get(user.role, (100, 3600)) @app.post("/api/v1/resource") async def resource_endpoint( current_user: User = Depends(get_current_user) ): max_req, window = get_rate_limit_for_user(current_user) client_id = f"user:{current_user.id}" if not rate_limiter.allow_request(client_id, max_req, window): raise RateLimitException(retry_after=window) return {"status": "success"} ``` ### Endpoint-Specific Limits ```python RATE_LIMITS = { "/api/v1/auth/login": (5, 300), # 5 per 5 minutes "/api/v1/products": (100, 3600), # 100 per hour "/api/v1/orders": (50, 3600), # 50 per hour } @app.middleware("http") async def rate_limit_middleware(request: Request, call_next): if request.url.path in RATE_LIMITS: max_req, window = RATE_LIMITS[request.url.path] client_id = request.client.host if not rate_limiter.allow_request(client_id, max_req, window): raise RateLimitException(retry_after=window) return await call_next(request) ``` ## Testing Rate Limits ### Unit Tests ```python import pytest from middleware.rate_limiter import RateLimiter def test_rate_limiter_allows_requests_within_limit(): limiter = RateLimiter() client = "test_client" # Should allow first 5 requests for i in range(5): assert limiter.allow_request(client, max_requests=5, window_seconds=60) # Should deny 6th request assert not limiter.allow_request(client, max_requests=5, window_seconds=60) ``` ### Integration Tests ```python def test_rate_limit_endpoint(client): # Make requests up to limit for i in range(10): response = client.post("/api/v1/resource") assert response.status_code == 200 # Next request should be rate limited response = client.post("/api/v1/resource") assert response.status_code == 429 assert "retry_after" in response.json() ``` ## Production Considerations ### Distributed Rate Limiting For multi-server deployments, use Redis: ```python import redis from datetime import datetime, timezone class RedisRateLimiter: def __init__(self): self.redis = redis.Redis(host='localhost', port=6379, db=0) def allow_request(self, client_id: str, max_requests: int, window: int) -> bool: key = f"ratelimit:{client_id}" now = datetime.now(timezone.utc).timestamp() # Remove old entries self.redis.zremrangebyscore(key, 0, now - window) # Count requests in window count = self.redis.zcard(key) if count < max_requests: # Add new request self.redis.zadd(key, {now: now}) self.redis.expire(key, window) return True return False ``` ### Monitoring Log rate limit violations for monitoring: ```python @app.middleware("http") async def rate_limit_monitoring(request: Request, call_next): try: response = await call_next(request) return response except RateLimitException as e: logger.warning( f"Rate limit exceeded", extra={ "client": request.client.host, "path": request.url.path, "user_agent": request.headers.get("user-agent") } ) raise ``` ## API Reference For detailed implementation, see the RateLimiter class in `middleware/rate_limiter.py`. ## Related Documentation - [Error Handling](error-handling.md) - HTTP error responses - [Authentication](authentication.md) - API authentication - [Error Handling](error-handling.md) - RateLimitException details - [Authentication](authentication.md) - User-based rate limiting