Problem Statement. createRateLimiter() in app/backend/src/common/security/security.module.ts
uses a process-locale Map<string, {count, resetTimeMs}> as its store. At scale, this
(a) wastes memory proportional to unique client IPs, (b) silently loses the limit counter when
the process restarts (an attacker simply widens their budget across restarts), (c) does not
share state across the many replicas one would deploy behind a load balancer.
Why it matters. Rate limiting is one of the cheapest, broadest defenses. A loss-of-state
or per-replica division of the limit is the difference between effective DoS mitigation and
incidental throttling.
Technical Context. The repo already imports both @liaoliaots/nestjs-redis and
@nestjs/throttler. The throttler module is configured with a global 20 req/min limit in
app.module.ts. The current createRateLimiter is registered as an Express middleware via
app.use(createRateLimiter(cs)) in main.ts. The store needs to be migrated.
Expected Outcome. A sliding-window limiter using a Redis sorted set, with all current
behaviour preserved (headers, exempt paths, isVerificationPath special case) and configurable
window/limit via env.
Acceptance Criteria.
- New
RedisSlidingWindowRateLimiter middleware with the same interface
((req, res, next) => void).
- Configurable via
RATE_LIMIT_WINDOW_MS and RATE_LIMIT_LIMIT envs.
- A new e2e test asserts: 100 hits in 1 s => 80+ return 429, headers include
RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, Retry-After.
- A redis-down test (use
ioredis-mock) asserts the limiter fails open with a WARN log,
not 500.
Implementation Notes. Replace the Map store with a ZADD/ZCOUNT/ZREMRANGEBYSCORE
per key; reuse the req.ips[0] fallback for the key. Use MULTI to keep header reads
consistent.
Files or modules likely to be affected. src/common/security/security.module.ts,
src/common/guards/adaptive-rate-limit.guard.ts, app/backend/test/security.e2e-spec.ts,
app.module.ts (registration order).
Dependencies. None (but mutually exclusive with the throttler guard in #46).
Difficulty. Hard
Estimated effort. M
Backlog item #4 from `docs/maintainer-issue-backlog.md.
Problem Statement.
createRateLimiter()inapp/backend/src/common/security/security.module.tsuses a process-locale
Map<string, {count, resetTimeMs}>as its store. At scale, this(a) wastes memory proportional to unique client IPs, (b) silently loses the limit counter when
the process restarts (an attacker simply widens their budget across restarts), (c) does not
share state across the many replicas one would deploy behind a load balancer.
Why it matters. Rate limiting is one of the cheapest, broadest defenses. A loss-of-state
or per-replica division of the limit is the difference between effective DoS mitigation and
incidental throttling.
Technical Context. The repo already imports both
@liaoliaots/nestjs-redisand@nestjs/throttler. The throttler module is configured with a global 20 req/min limit inapp.module.ts. The currentcreateRateLimiteris registered as an Express middleware viaapp.use(createRateLimiter(cs))inmain.ts. The store needs to be migrated.Expected Outcome. A sliding-window limiter using a Redis sorted set, with all current
behaviour preserved (headers, exempt paths, isVerificationPath special case) and configurable
window/limit via env.
Acceptance Criteria.
RedisSlidingWindowRateLimitermiddleware with the same interface(
(req, res, next) => void).RATE_LIMIT_WINDOW_MSandRATE_LIMIT_LIMITenvs.RateLimit-Limit,RateLimit-Remaining,RateLimit-Reset,Retry-After.ioredis-mock) asserts the limiter fails open with a WARN log,not 500.
Implementation Notes. Replace the
Mapstore with aZADD/ZCOUNT/ZREMRANGEBYSCOREper key; reuse the
req.ips[0]fallback for the key. UseMULTIto keep header readsconsistent.
Files or modules likely to be affected.
src/common/security/security.module.ts,src/common/guards/adaptive-rate-limit.guard.ts,app/backend/test/security.e2e-spec.ts,app.module.ts(registration order).Dependencies. None (but mutually exclusive with the throttler guard in #46).
Difficulty. Hard
Estimated effort. M
Backlog item #4 from `docs/maintainer-issue-backlog.md.