// In-memory sliding-window rate limiter. // // State is module-scoped: each Next.js runtime (Edge middleware vs Node.js server // components) gets its own module instance, so buckets are not shared between them. // // Redis migration path: replace the `buckets` Map with an upstash/ratelimit or // ioredis ZADD + ZRANGEBYSCORE approach. The exported function signatures stay the // same, only the storage layer changes, giving cross-process and cross-runtime // consistency for multi-replica deployments. const WINDOW_MS = 60_000; // 1 minute const LIMIT = 50; // max attempts per window per key const MAX_KEYS = 10_000; // evict oldest when Map grows beyond this const buckets = new Map(); function sweep(key: string, now: number): number[] { const fresh = (buckets.get(key) ?? []).filter((t) => now - t < WINDOW_MS); buckets.set(key, fresh); return fresh; } function evictOldest(exclude: string) { if (buckets.size < MAX_KEYS) return; for (const k of buckets.keys()) { if (k !== exclude) { buckets.delete(k); return; } } } /** * Check whether `key` has exceeded the rate limit WITHOUT recording a new attempt. * Use this to gate a request before you know whether it will succeed or fail. */ export function isRateLimited(key: string): boolean { return sweep(key, Date.now()).length >= LIMIT; } /** * Record one failed attempt for `key`. * Successful requests should NOT call this — only failures increment the bucket. */ export function recordFailure(key: string): void { const now = Date.now(); const ts = sweep(key, now); if (ts.length >= LIMIT) return; // already over limit; don't bother growing the array evictOldest(key); buckets.set(key, [...ts, now]); } /** * Check and atomically consume one token for `key`. * Returns `true` if the request is allowed; `false` if rate-limited. * Used by middleware where every inbound request (success or failure) is counted. */ export function consume(key: string): boolean { const now = Date.now(); const ts = sweep(key, now); if (ts.length >= LIMIT) return false; evictOldest(key); buckets.set(key, [...ts, now]); return true; }