# 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
