Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions backend/middlewares/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
};
};
Expand Down
Loading