NestJS + Redis: Distributed Rate-Limiting and Brute-Force Protection
When your API runs behind more than one replica, in-memory rate limits quietly let attackers through. Here is how to do rate-limiting and account lockout correctly with Redis in NestJS.
A throttler that lives in process memory is correct on a single replica and wrong the moment you run two. If the limit is "5 logins per minute per IP" and you have three replicas behind a load balancer, an attacker who sprays requests across all three gets 15 attempts — the in-memory counters never add up. The fix is shared state, and Redis is the right shared state for this.
Redis-backed throttler storage
With @nest-lab/throttler-storage-redis, every replica reads and increments the same counter in Redis. The limit is now global, not per-process. This is not an optimization — for brute-force protection it is the only correct design.
Timing equalization
A subtler leak: response timing. If bcrypt.compare only runs when the email exists, an attacker can enumerate accounts by measuring which requests take longer. The fix is to always run a comparison — against a dummy hash when the account does not exist — so every login path takes the same time.
Account lockout, done carefully
We lock after 5 consecutive failures for 15 minutes. Two non-obvious rules make this safe:
- Never lock inactive accounts. Otherwise an attacker can lock-out DoS accounts you have not yet activated, and admins can never recover them.
- Reset the counter on success. A user who fails four times, remembers their password, and succeeds should start fresh — not sit one failure away from a lock.
Hard-coded thresholds, not config knobs
The 5-attempt and 15-minute values are product decisions, not operational settings. Exposing them as config invites someone to "tune" them into a vulnerability. Keep them in source, review them in PRs, and keep Redis as the single source of truth for the counters themselves.
