# ADR-032: Rate Limiting Strategy **Date**: 2026-01-09 **Status**: Accepted **Implemented**: 2026-01-09 ## Context Public-facing APIs are vulnerable to abuse through excessive requests, whether from malicious actors attempting denial-of-service attacks, automated scrapers, or accidental loops in client code. Without proper rate limiting, the application could: 1. **Experience degraded performance**: Excessive requests can overwhelm database connections and server resources 2. **Incur unexpected costs**: AI service calls (Gemini API) and external APIs (Google Maps) are billed per request 3. **Allow credential stuffing**: Login endpoints without limits enable brute-force attacks 4. **Suffer from data scraping**: Public endpoints could be scraped at high volume ## Decision We will implement a tiered rate limiting strategy using `express-rate-limit` middleware, with different limits based on endpoint sensitivity and resource cost. ### Tier System | Tier | Window | Max Requests | Use Case | | --------------------------- | ------ | ------------ | -------------------------------- | | **Authentication (Strict)** | 15 min | 5 | Login, registration | | **Sensitive Operations** | 1 hour | 5 | Password changes, email updates | | **AI/Costly Operations** | 15 min | 10-20 | Gemini API calls, geocoding | | **File Uploads** | 15 min | 10-20 | Flyer uploads, avatar uploads | | **Batch Operations** | 15 min | 50 | Bulk updates | | **User Read** | 15 min | 100 | Standard authenticated endpoints | | **Public Read** | 15 min | 100 | Public data endpoints | | **Tracking/High-Volume** | 15 min | 150-200 | Analytics, reactions | ### Rate Limiter Configuration All rate limiters share a standard configuration: ```typescript const standardConfig = { standardHeaders: true, // Return rate limit info in headers legacyHeaders: false, // Disable deprecated X-RateLimit headers skip: shouldSkipRateLimit, // Allow bypassing in test environment }; ``` ### Test Environment Bypass Rate limiting is bypassed during integration and E2E tests to avoid test flakiness: ```typescript export const shouldSkipRateLimit = (req: Request): boolean => { return process.env.NODE_ENV === 'test'; }; ``` ## Implementation Details ### Available Rate Limiters | Limiter | Window | Max | Endpoint Examples | | ---------------------------- | ------ | --- | --------------------------------- | | `loginLimiter` | 15 min | 5 | POST /api/auth/login | | `registerLimiter` | 1 hour | 5 | POST /api/auth/register | | `forgotPasswordLimiter` | 15 min | 5 | POST /api/auth/forgot-password | | `resetPasswordLimiter` | 15 min | 10 | POST /api/auth/reset-password | | `refreshTokenLimiter` | 15 min | 20 | POST /api/auth/refresh | | `logoutLimiter` | 15 min | 10 | POST /api/auth/logout | | `publicReadLimiter` | 15 min | 100 | GET /api/flyers, GET /api/recipes | | `userReadLimiter` | 15 min | 100 | GET /api/users/profile | | `userUpdateLimiter` | 15 min | 100 | PUT /api/users/profile | | `userSensitiveUpdateLimiter` | 1 hour | 5 | PUT /api/auth/change-password | | `adminTriggerLimiter` | 15 min | 30 | POST /api/admin/jobs/\* | | `aiGenerationLimiter` | 15 min | 20 | POST /api/ai/analyze | | `aiUploadLimiter` | 15 min | 10 | POST /api/ai/upload-and-process | | `geocodeLimiter` | 1 hour | 100 | GET /api/users/geocode | | `priceHistoryLimiter` | 15 min | 50 | GET /api/price-history/\* | | `reactionToggleLimiter` | 15 min | 150 | POST /api/reactions/toggle | | `trackingLimiter` | 15 min | 200 | POST /api/personalization/track | | `batchLimiter` | 15 min | 50 | PATCH /api/budgets/batch | ### Usage Pattern ```typescript import { loginLimiter, userReadLimiter } from '../config/rateLimiters'; // Apply to individual routes router.post('/login', loginLimiter, validateRequest(loginSchema), async (req, res, next) => { // handler }); // Or apply to entire router for consistent limits router.use(userReadLimiter); router.get('/me', async (req, res, next) => { /* handler */ }); ``` ### Response Headers When rate limiting is active, responses include standard headers: ``` RateLimit-Limit: 100 RateLimit-Remaining: 95 RateLimit-Reset: 900 ``` ### Rate Limit Exceeded Response When a client exceeds their limit: ```json { "message": "Too many login attempts from this IP, please try again after 15 minutes." } ``` HTTP Status: `429 Too Many Requests` ## Key Files - `src/config/rateLimiters.ts` - Rate limiter definitions - `src/utils/rateLimit.ts` - Helper functions (test bypass) ## Consequences ### Positive - **Security**: Protects against brute-force and credential stuffing attacks - **Cost Control**: Prevents runaway costs from AI/external API abuse - **Fair Usage**: Ensures all users get reasonable service access - **DDoS Mitigation**: Provides basic protection against request flooding ### Negative - **Legitimate User Impact**: Aggressive users may hit limits during normal use - **IP-Based Limitations**: Shared IPs (offices, VPNs) may cause false positives - **No Distributed State**: Rate limits are per-instance, not cluster-wide (would need Redis store for that) ## Future Enhancements 1. **Redis Store**: Implement distributed rate limiting with Redis for multi-instance deployments 2. **User-Based Limits**: Track limits per authenticated user rather than just IP 3. **Dynamic Limits**: Adjust limits based on user tier (free vs premium) 4. **Monitoring Dashboard**: Track rate limit hits in admin dashboard 5. **Allowlisting**: Allow specific IPs (monitoring services) to bypass limits