6.0 KiB
6.0 KiB
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:
- Experience degraded performance: Excessive requests can overwhelm database connections and server resources
- Incur unexpected costs: AI service calls (Gemini API) and external APIs (Google Maps) are billed per request
- Allow credential stuffing: Login endpoints without limits enable brute-force attacks
- 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:
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:
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
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:
{
"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 definitionssrc/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
- Redis Store: Implement distributed rate limiting with Redis for multi-instance deployments
- User-Based Limits: Track limits per authenticated user rather than just IP
- Dynamic Limits: Adjust limits based on user tier (free vs premium)
- Monitoring Dashboard: Track rate limit hits in admin dashboard
- Allowlisting: Allow specific IPs (monitoring services) to bypass limits