From 869de78f3e337d82344c0e7e2e916065e39dba89 Mon Sep 17 00:00:00 2001 From: Minit Date: Fri, 17 Apr 2026 22:42:03 +0530 Subject: [PATCH 1/2] fix(api): evict only non-rate-limited entries when map exceeds capacity Previous code called requestCounts.clear() when size exceeded 10k entries after pruning expired ones, resetting rate limit state for all clients including those already being blocked. Now evicts only entries with count below the rate limit threshold, stopping once size is within bounds. Clients at or above the limit keep their state. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/app/api/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/utils.ts b/apps/web/app/api/utils.ts index 1feeb4db91..7d992dbb41 100644 --- a/apps/web/app/api/utils.ts +++ b/apps/web/app/api/utils.ts @@ -34,7 +34,10 @@ export const developerRateLimiter = createMiddleware(async (c, next) => { if (now > v.resetAt) requestCounts.delete(k); } if (requestCounts.size > RATE_LIMIT_MAX_ENTRIES) { - requestCounts.clear(); + for (const [k, v] of requestCounts) { + if (requestCounts.size <= RATE_LIMIT_MAX_ENTRIES) break; + if (v.count < RATE_LIMIT_MAX_REQUESTS) requestCounts.delete(k); + } } } From 83a0eee2916b4e2f46fb5105c304ef082cc6479d Mon Sep 17 00:00:00 2001 From: Minit Date: Fri, 17 Apr 2026 22:47:31 +0530 Subject: [PATCH 2/2] fix(api): fallback to soonest-expiring eviction when all entries are rate-limited Co-Authored-By: Claude Sonnet 4.6 --- apps/web/app/api/utils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/web/app/api/utils.ts b/apps/web/app/api/utils.ts index 7d992dbb41..0eb33aa57a 100644 --- a/apps/web/app/api/utils.ts +++ b/apps/web/app/api/utils.ts @@ -38,6 +38,15 @@ export const developerRateLimiter = createMiddleware(async (c, next) => { if (requestCounts.size <= RATE_LIMIT_MAX_ENTRIES) break; if (v.count < RATE_LIMIT_MAX_REQUESTS) requestCounts.delete(k); } + if (requestCounts.size > RATE_LIMIT_MAX_ENTRIES) { + const byExpiry = [...requestCounts.entries()].sort( + (a, b) => a[1].resetAt - b[1].resetAt, + ); + for (const [k] of byExpiry) { + if (requestCounts.size <= RATE_LIMIT_MAX_ENTRIES) break; + requestCounts.delete(k); + } + } } }