From 9a7b07693fa6ad7d47f35995cc7c501eb198f5a1 Mon Sep 17 00:00:00 2001 From: Shreya-nipunge Date: Wed, 3 Jun 2026 20:32:16 +0530 Subject: [PATCH] improve: enhance in-memory rate limiter with headers and safety --- backend/middlewares/rateLimiter.js | 33 ++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/backend/middlewares/rateLimiter.js b/backend/middlewares/rateLimiter.js index 0f67426..d00bd5c 100644 --- a/backend/middlewares/rateLimiter.js +++ b/backend/middlewares/rateLimiter.js @@ -1,3 +1,16 @@ +/** + * Lightweight, in-memory rate limiter. + * + * DESIGN DECISION: + * This rate limiter stores request tracking data in a local Node.js Map. + * - PRO: Extremely fast (zero latency), zero infrastructure dependency. + * - CON: State resets on server restart, and is per-instance (not shared horizontally). + * + * For this project's current scale, this trade-off is accepted. + * If distributed rate-limiting is required in the future (e.g., across multiple servers), + * this can be extended to use Redis or a Supabase UNLOGGED table. + */ + const WINDOW_MS = 60 * 1000; const MAX_REQUESTS = 20; const MAX_ENTRIES = 10000; @@ -36,6 +49,7 @@ export const createRateLimiter = (options = {}) => { const key = deriveRateLimitKey(req); const now = Date.now(); + // Periodic cleanup of stale entries if (now - cleanupTime >= CLEANUP_INTERVAL_MS) { for (const [k, entry] of store.entries()) { if (now - entry.windowStart >= windowMs) { @@ -47,24 +61,35 @@ export const createRateLimiter = (options = {}) => { let entry = store.get(key); + // If new user or window expired, create a new tracking entry if (!entry || now - entry.windowStart >= windowMs) { + // Prevent memory leaks by capping the Map size if (!entry && store.size >= maxEntries) { const oldestKey = store.keys().next().value; if (oldestKey !== undefined) { store.delete(oldestKey); } } - store.set(key, { count: 1, windowStart: now }); - return next(); + entry = { count: 1, windowStart: now }; + store.set(key, entry); + } else { + entry.count += 1; } - if (entry.count >= maxRequests) { + // Set standard RateLimit headers for better API UX + const remaining = Math.max(0, maxRequests - entry.count); + const resetTime = new Date(entry.windowStart + windowMs); + + res.setHeader('X-RateLimit-Limit', maxRequests); + res.setHeader('X-RateLimit-Remaining', remaining); + res.setHeader('X-RateLimit-Reset', Math.ceil(resetTime.getTime() / 1000)); + + if (entry.count > maxRequests) { return res.status(429).json({ error: "Too many requests. Please wait before sending more messages.", }); } - entry.count += 1; next(); }; };