Skip to content

Replace in-process Map-based rate limiter with a Redis ZSET sliding-window #212

Description

@kilodesodiq-arch

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.

Metadata

Metadata

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions