fix(rate-limiter): use max duration for cleanup
- commit
- fe686143cfdc528cdf481a4b2a8ec39a27b0127d
- author
- bnema · · 2026-01-18 21:47 UTC
- parent
- 7119e85
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +12 -3
M src/lib/api/rate-limiter.ts +12 -3
| @@ -41,17 +41,26 @@ const RATE_LIMITS = { | |||
| 41 | 41 | // In-memory store for rate limiting | |
| 42 | 42 | const ipRequests = new Map<string, Array<{ timestamp: number }>>(); | |
| 43 | 43 | ||
| 44 | + | // Compute max duration across all rate limits for cleanup | |
| 45 | + | const MAX_RATE_LIMIT_DURATION = Math.max( | |
| 46 | + | RATE_LIMITS.auth.durationMs, | |
| 47 | + | RATE_LIMITS.public.durationMs, | |
| 48 | + | RATE_LIMITS.protected.durationMs, | |
| 49 | + | RATE_LIMITS.sse.authenticated.durationMs, | |
| 50 | + | RATE_LIMITS.sse.unauthenticated.durationMs | |
| 51 | + | ); | |
| 52 | + | ||
| 44 | 53 | // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope) | |
| 45 | 54 | let lastCleanup = 0; | |
| 46 | - | const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes | |
| 47 | 55 | ||
| 48 | 56 | function cleanupStaleEntries() { | |
| 49 | 57 | const now = Date.now(); | |
| 50 | - | if (now - lastCleanup < CLEANUP_INTERVAL) return; | |
| 58 | + | // Run cleanup at most once per max duration window | |
| 59 | + | if (now - lastCleanup < MAX_RATE_LIMIT_DURATION) return; | |
| 51 | 60 | lastCleanup = now; | |
| 52 | 61 | ||
| 53 | 62 | for (const [ip, timestamps] of ipRequests.entries()) { | |
| 54 | - | const filtered = timestamps.filter((t) => now - t.timestamp < CLEANUP_INTERVAL); | |
| 63 | + | const filtered = timestamps.filter((t) => now - t.timestamp < MAX_RATE_LIMIT_DURATION); | |
| 55 | 64 | if (filtered.length === 0) { | |
| 56 | 65 | ipRequests.delete(ip); | |
| 57 | 66 | } else { | |
| @@ -41,17 +41,26 @@ const RATE_LIMITS = { | |||||
| 41 | // In-memory store for rate limiting | 41 | // In-memory store for rate limiting | ||
| 42 | const ipRequests = new Map<string, Array<{ timestamp: number }>>(); | 42 | const ipRequests = new Map<string, Array<{ timestamp: number }>>(); | ||
| 43 | 43 | ||||
| 44 | + | // Compute max duration across all rate limits for cleanup | |||
| 45 | + | const MAX_RATE_LIMIT_DURATION = Math.max( | |||
| 46 | + | RATE_LIMITS.auth.durationMs, | |||
| 47 | + | RATE_LIMITS.public.durationMs, | |||
| 48 | + | RATE_LIMITS.protected.durationMs, | |||
| 49 | + | RATE_LIMITS.sse.authenticated.durationMs, | |||
| 50 | + | RATE_LIMITS.sse.unauthenticated.durationMs | |||
| 51 | + | ); | |||
| 52 | + | ||||
| 44 | // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope) | 53 | // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope) | ||
| 45 | let lastCleanup = 0; | 54 | let lastCleanup = 0; | ||
| 46 | - | const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes | |||
| 47 | 55 | ||||
| 48 | function cleanupStaleEntries() { | 56 | function cleanupStaleEntries() { | ||
| 49 | const now = Date.now(); | 57 | const now = Date.now(); | ||
| 50 | - | if (now - lastCleanup < CLEANUP_INTERVAL) return; | 58 | + | // Run cleanup at most once per max duration window |
| 59 | + | if (now - lastCleanup < MAX_RATE_LIMIT_DURATION) return; | |||
| 51 | lastCleanup = now; | 60 | lastCleanup = now; | ||
| 52 | 61 | ||||
| 53 | for (const [ip, timestamps] of ipRequests.entries()) { | 62 | for (const [ip, timestamps] of ipRequests.entries()) { | ||
| 54 | - | const filtered = timestamps.filter((t) => now - t.timestamp < CLEANUP_INTERVAL); | 63 | + | const filtered = timestamps.filter((t) => now - t.timestamp < MAX_RATE_LIMIT_DURATION); |
| 55 | if (filtered.length === 0) { | 64 | if (filtered.length === 0) { | ||
| 56 | ipRequests.delete(ip); | 65 | ipRequests.delete(ip); | ||
| 57 | } else { | 66 | } else { | ||