Files
flyer-crawler.projectium.com/docs/adr/0032-rate-limiting-strategy.md

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:

  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:

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 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