Skip to content

Add a Redis-backed rate-limit store so per-key limits hold across horizontally scaled instances #231

Description

@greatest0fallt1me

Description

The rate limiter in src/middleware/rateLimit.ts stores buckets in a per-process Map, and its own docstring states a "horizontally scaled deployment should swap the storage for Redis without touching the middleware's public surface". Today, running multiple API replicas multiplies the effective limit by the replica count and lets the /api/risk/evaluate cap (RATE_LIMIT_MAX_EVALUATE, default 10, from src/config/rateLimit.ts) be bypassed. This issue introduces a pluggable store abstraction with an in-memory default and an optional Redis backend.

Requirements and context

  • Extract a RateLimitStore interface (atomic increment(key, windowMs) returning { count, resetAt }) and refactor createRateLimitMiddleware to depend on it without changing RateLimitOptions, the X-RateLimit-*/Retry-After headers, or the { data, error, retryAfter } 429 envelope.
  • Provide an in-memory store (current behavior, preserving the existing cleanup() sweep) and a Redis store gated by an env var (e.g. RATE_LIMIT_REDIS_URL); when unset, behavior is unchanged.
  • Use an atomic Redis primitive (e.g. INCR + PEXPIRE) so concurrent requests across replicas share one window; reuse the key generators createIpKeyGenerator / createApiKeyKeyGenerator unchanged.
  • Validate the new env var in src/config/env.ts so misconfiguration fails fast, consistent with the existing Zod schema.
  • Non-functional: a Redis outage must degrade gracefully (fail-open or fail-closed by config) rather than 500-ing every request; document which mode is default.

Acceptance criteria

  • RateLimitStore interface exists with in-memory and Redis implementations.
  • Middleware public surface, headers, and 429 envelope are byte-for-byte unchanged for the in-memory path.
  • With the Redis store, two simulated instances sharing one key observe a single shared counter.
  • RATE_LIMIT_REDIS_URL (or equivalent) is validated at startup via the env schema.
  • Redis connection failure follows a documented, tested degradation mode.
  • docs/SECURITY.md §5 (rate-limit tuning) documents the Redis option and the degradation behavior.

Suggested execution

1. Fork the repo and create a branchgit checkout -b feature/redis-rate-limit-store.
2. Implement changes — refactor src/middleware/rateLimit.ts, add stores under src/middleware/ or src/config/, and extend src/config/env.ts.
3. Write/extend tests — Vitest + Supertest; extend tests/middleware/rateLimit.test.ts and tests/config/rateLimit.test.ts, mocking the Redis client for the shared-counter and outage cases.
4. Test and commit

npm run lint
npm run typecheck
npm test
npm run test:coverage
npm run build

Example commit message

feat(ratelimit): add pluggable store with optional Redis backend

Guidelines

CI enforces a 95% coverage threshold (vitest.config.ts, via npm run test:coverage) — cover the store interface, both backends, and the outage path. Update docs/SECURITY.md and .env.example. Timeframe: 96 hours.

Metadata

Metadata

Assignees

Labels

Type

No type
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