perf(rate-limit): evict expired keys so the Map stays bounded - #8
Merged
Conversation
WHY: InMemoryRateLimiter keyed on client IP but never removed a key once its sliding window had fully elapsed — check()/remaining() always stored the pruned timestamp list (even when empty) and there was no sweep. On a long-lived warm Vercel instance this is a slow but real memory leak: one permanent Map entry for every distinct IP that ever POSTed /api/lead, retained for the instance's whole lifetime even after the IP went quiet. WHAT: - Add a private sweep() that deletes every key whose newest timestamp has aged out of the window; call it opportunistically at the top of check() so the Map is bounded to currently-active keys (O(live keys)). Uses Map.forEach + a collected delete list to stay within the project's tsconfig target (no downlevel iteration). - In remaining(), delete the key when its pruned list is empty instead of leaving an empty-array entry behind. - Expose a read-only `size` getter so the bound can be asserted in tests. - Add regression tests: a key is evicted once its window elapses, size returns toward 0 as keys age out, a stream of 50 unique IPs never grows the Map past 1 entry, remaining() leaves no stale entry, and the limit is still enforced for active keys (no behavior regression). Co-authored-by: mattia-mamini-gh <281593356+mattia-mamini-gh@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
InMemoryRateLimiter(lib/rate-limit.ts) keys on client IP but never removes a key once its sliding window has fully elapsed.check()/remaining()always store the pruned timestamp list (even when it is empty) and there is no sweep. On a long-lived warm Vercel function instance this is a slow but real memory leak: one permanentMapentry per distinct IP that ever POSTed/api/lead, retained for the instance's whole lifetime even after that IP goes quiet.What
sweep()that deletes every key whose newest timestamp has aged out of the window, called opportunistically at the top ofcheck(), so theMapstays bounded to currently-active keys (O(live keys), not O(all keys ever seen)). Implemented withMap.forEach+ a collected delete list to stay within the project'stsconfigtarget (no--downlevelIterationneeded).remaining(),deletethe key when its pruned list is empty instead of leaving an empty-array entry behind.sizegetter so the bound is observable/assertable.sizecollapses toward 0 as keys age out; a stream of 50 unique IPs never grows theMappast 1 entry;remaining()leaves no stale entry; and the limit is still enforced for active keys (no behavior regression).Verification
npm run typecheck— passnpm run lint— pass (no warnings/errors)npm test— pass (235 tests, +5 new rate-limiter guards)npm run build— passsize) and pass after the fix; the no-regression test passes on both.Co-authored-by: mattia-mamini-gh 281593356+mattia-mamini-gh@users.noreply.github.com