-
Notifications
You must be signed in to change notification settings - Fork 96
Bound and evict stale buckets in the in-memory rate limiter #299
Copy link
Copy link
Closed
Labels
GrantFox OSSGrantFox Open Source Sponsorship program tagGrantFox Open Source Sponsorship program tagMaybe RewardedIssue may qualify for a reward upon successful completion per campaign rulesIssue may qualify for a reward upon successful completion per campaign rulesThird CampaignOfficial FWC26 campaign issue — eligible for campaign scoring and rewardsOfficial FWC26 campaign issue — eligible for campaign scoring and rewardsapi-layerAutomatically createdAutomatically createdbugConfirmed defect or incorrect behavior that needs to be fixedConfirmed defect or incorrect behavior that needs to be fixedpriority: mediumAutomatically createdAutomatically created
Description
Activity
Metadata
Metadata
Assignees
Labels
GrantFox OSSGrantFox Open Source Sponsorship program tagGrantFox Open Source Sponsorship program tagMaybe RewardedIssue may qualify for a reward upon successful completion per campaign rulesIssue may qualify for a reward upon successful completion per campaign rulesThird CampaignOfficial FWC26 campaign issue — eligible for campaign scoring and rewardsOfficial FWC26 campaign issue — eligible for campaign scoring and rewardsapi-layerAutomatically createdAutomatically createdbugConfirmed defect or incorrect behavior that needs to be fixedConfirmed defect or incorrect behavior that needs to be fixedpriority: mediumAutomatically createdAutomatically created
Difficulty: Advanced
Type: performance
Background
lib/rate-limit.tsimplements a token-bucket rate limiter for the integration gateway routes, keyed by`ip:${ip}`and`wallet:${address}`. Buckets are stored in a module-levelMap<string, Bucket>(const buckets = new Map<string, Bucket>()), created lazily on first request viagetBucket()and never removed.Problem
There is no eviction of stale entries. Every unique IP or wallet address that ever calls a rate-limited route (
/api/integration/membership,/api/integration/verify) creates a permanent entry inbucketsfor the lifetime of the Node process. On a long-running instance exposed to the public internet, this is an unbounded-growth memory leak — an attacker could also deliberately spray requests from many spoofedX-Forwarded-Forvalues to inflate memory usage, since the IP key is derived directly from a client-controllable header with no allowlist of trusted proxies.Expected Outcome
The rate limiter's memory footprint stays bounded regardless of how many distinct IPs/wallets have made requests over the process lifetime, without changing the observable rate-limiting behavior (
RateLimitResultsemantics,retryAfter,remaining) for any individual key.Suggested Implementation
lastAccesstimestamp to theBucketinterface (or reuselast) and implement a periodic or on-access sweep that deletes buckets whose tokens are fully refilled (i.e., inactive for at leastWINDOW_MS) and haven't been touched recently — e.g., prune entries idle longer thanN × WINDOW_MS.MAX_BUCKETS) combined with lazy sweep-on-write, or asetIntervalsweep guarded to only run in a real server runtime (not during tests/edge builds).take()calls (Node's request handling is single-threaded per event-loop tick, but be careful with anyasyncboundaries introduced).resetLiveApiResilienceState()inlib/api/live.tsfor the circuit breaker).test/rate-limit.test.tswith tests asserting the bucket map does not grow unboundedly after many distinct keys go idle.Acceptance Criteria
limited,retryAfter,remainingfor active keys) is unchanged — all currenttest/rate-limit.test.tsassertions still pass.npm run typecheck,npm run lint,npm testpass.Likely Affected Files/Directories
lib/rate-limit.tstest/rate-limit.test.tsdocs/deployment.md(if it references rate-limiter behavior)