1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116# Rate limits reference
Configured limits, Redis key patterns, and response headers. For the why, see [rate limiting design](../explanation/rate-limiting.md).
## Configured tiers
| Tier | Scope | Limit | Window | Purpose |
|---|---|---|---|---|
| Per-IP submission | `X-Forwarded-For` first IP | 10 | 1 hour | Individual abuse / runaway scripts |
| Per-IP status check | `X-Forwarded-For` first IP | 100 | 1 hour | Aggressive polling |
| Global submission | System-wide | 1000 | 24 hours | Cost ceiling |
All tiers return **HTTP 429** with a `Retry-After` header on violation. Adjust values in `src/services/rate_limit_service.py`:
```python
class RateLimitService:
SUBMIT_PER_IP_LIMIT = 10 # Submissions per hour per IP
SUBMIT_PER_IP_WINDOW = 3600
STATUS_PER_IP_LIMIT = 100 # Status checks per hour per IP
STATUS_PER_IP_WINDOW = 3600
GLOBAL_SUBMIT_LIMIT = 1000 # Global submissions per day
GLOBAL_SUBMIT_WINDOW = 86400
```
## Response headers
Every rate-limited response includes:
```http
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1704124800
```
On 429:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 300
X-RateLimit-Remaining: 0
{
"detail": "Rate limit exceeded for submission",
"retry_after": 300,
"limit_type": "submission"
}
```
## Exempt endpoints
The following bypass rate limiting:
- `/` โ viewer SPA
- `/health`, `/health/ready` โ load-balancer checks
- `/docs`, `/redoc`, `/openapi.json` โ API documentation
- `/metrics` โ Prometheus scrape
## Redis keys
```
eq-pdf:ratelimit:submit:ip:{client_ip}
eq-pdf:ratelimit:status:ip:{client_ip}
eq-pdf:ratelimit:submit:global
```
Each key is a sorted set with timestamp scores and an `EXPIRE` cleanup TTL matching the window. Example payload:
```
ZADD eq-pdf:ratelimit:submit:ip:192.168.1.1 1704120000.123 "req-1"
ZADD eq-pdf:ratelimit:submit:ip:192.168.1.1 1704120005.456 "req-2"
EXPIRE eq-pdf:ratelimit:submit:ip:192.168.1.1 3600
```
## Client IP detection
Priority order in the middleware:
1. `X-Forwarded-For` โ first IP in the chain
2. `X-Real-IP`
3. `request.client.host` โ direct connection fallback
In production, ensure the ALB or reverse proxy sets trusted `X-Forwarded-For` headers.
## Environment variables
```bash
REDIS_URL=redis://redis:6379 # Required
REDIS_MAX_CONNECTIONS=10
```
## Administrative operations
```python
from src.services.rate_limit_service import RateLimitService
# Reset a tier for a specific IP
await rate_limiter.reset_rate_limit("192.168.1.100", "submit")
await rate_limiter.reset_rate_limit("192.168.1.100", "status")
# Check remaining quota
quota = await rate_limiter.get_remaining_quota("192.168.1.100", "submit")
# โ {"limit": 10, "remaining": 3, "reset_at": 1704124800, "window_seconds": 3600}
```
## Implementation
| File | Role |
|---|---|
| `src/services/rate_limit_service.py` | Sliding-window logic, Redis operations |
| `src/middleware/rate_limit.py` | FastAPI middleware, fail-open handling |
| `src/dependencies.py::get_rate_limit_service` | DI factory |
| `tests/unit/services/test_rate_limit_service.py` | Algorithm tests |
| `tests/unit/middleware/test_rate_limit.py` | Middleware tests (exemption, fail-open) |