Difficulty: Advanced
Type: performance
Background
lib/rate-limit.ts implements a token-bucket rate limiter for the integration gateway routes, keyed by `ip:${ip}` and `wallet:${address}`. Buckets are stored in a module-level Map<string, Bucket> (const buckets = new Map<string, Bucket>()), created lazily on first request via getBucket() 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 in buckets for 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 spoofed X-Forwarded-For values 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 (RateLimitResult semantics, retryAfter, remaining) for any individual key.
Suggested Implementation
- Add a
lastAccess timestamp to the Bucket interface (or reuse last) and implement a periodic or on-access sweep that deletes buckets whose tokens are fully refilled (i.e., inactive for at least WINDOW_MS) and haven't been touched recently — e.g., prune entries idle longer than N × WINDOW_MS.
- Choose an eviction strategy appropriate for a single Node process without adding new runtime dependencies — e.g., an LRU cap (
MAX_BUCKETS) combined with lazy sweep-on-write, or a setInterval sweep guarded to only run in a real server runtime (not during tests/edge builds).
- Ensure the sweep doesn't introduce race conditions with concurrent
take() calls (Node's request handling is single-threaded per event-loop tick, but be careful with any async boundaries introduced).
- Add a way to reset/inspect bucket count for tests (mirroring
resetLiveApiResilienceState() in lib/api/live.ts for the circuit breaker).
- Update the file's doc comment (currently describing the "SINGLE-INSTANCE CAVEAT") to describe the new eviction behavior.
- Extend
test/rate-limit.test.ts with tests asserting the bucket map does not grow unboundedly after many distinct keys go idle.
Acceptance Criteria
Likely Affected Files/Directories
lib/rate-limit.ts
test/rate-limit.test.ts
docs/deployment.md (if it references rate-limiter behavior)
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)