How rate-limiting works

Lesson#3 of 3 in topic Theory

Django

Django has no built-in brute-force protection and relies on packages.

Django delegates this entirely to third-party packages. The most common production setup uses django-axes.



django-axes hooks into Django's auth signals (user_logged_in_failed) and stores every failed attempt in an AccessAttempt DB table. Once the count hits AXES_FAILURE_LIMIT (default: 3), it locks by IP, username, or both. Unlocking can be automatic after a cooldown period or manual via the admin panel. There's no built-in equivalent in Django core — you must add this package.

Laravel

Laravel ships rate limiting natively. The ThrottleRequests middleware has been a first-class citizen since Laravel 8.

Laravel's RateLimiter uses your cache backend (Redis, Memcached, DB) to store attempt counts keyed by email|IP. The throttle:login middleware group is applied directly on the route. On success, RateLimiter::clear() resets the counter. A notable advantage over Django-axes: no DB migrations needed — it lives entirely in cache, making it faster under high load.

Express (NodeJS)

Express/Node has no opinion at all — the ecosystem answer is express-rate-limit, typically backed by a Redis store.

Express gives you the most flexibility but also the most responsibility. Key things to know:

  • express-rate-limit by default uses in-memory storage, which means limits reset on server restart and don't work across multiple instances. In production you must swap in rate-limit-redis or rate-limit-mongo.
  • The keyGenerator option is critical — defaulting to IP alone is weak (shared NAT, VPNs). A better key is sha256(email + IP) so one compromised network can't lock out unrelated accounts.
  • express-slow-down is a companion package worth pairing with it — instead of hard-blocking at attempt 5, it adds increasing artificial delay from attempt 1, which punishes bots without frustrating real users who mistype once.