Rate limiting, sliding windows & 429
Slow down brute force
Lesson 18 of 20 · 13 min · Module 5: Server-side defences
Attackers try thousands of passwords per minute, often from many addresses at once. Limiting how quickly anyone can try makes guessing impractical, without getting in the way of real people.
Start this lesson →Free. No installation: you write and run the code in your browser, and it is checked automatically.
By the end, you’ll be able to…
- Explain how rate limits slow down guessing
- Count attempts per key in a sliding window
- Answer with 429 and Retry-After
The idea
A rate limiter counts attempts per key, such as a username or an IP address, within a time window. Once the count reaches the limit, the server answers 429 Too Many Requests with a Retry-After header saying how many seconds to wait. Keeping the times of recent attempts gives a sliding window: an attempt expires exactly one window after it happened.
const times = [0, 1000, 2000];
const now = 61500, windowMs = 60000;
console.log(times.filter(time => time > now - windowMs)); // [2000]
01Make guessing slow
Five attempts per minute for an account allows 7,200 guesses a day instead of millions. Combined with good password rules, that is far too slow for guessing to work.
02Count per key, over time
Keep the times of recent attempts for each key and forget the old ones. Limit per account and per IP address together, because attackers spread their guesses over many accounts or over many addresses.
const recent = times.filter(time => time > now - windowMs);
03Be kind to real people
A hard lockout lets anyone lock a known person out on purpose. Prefer growing delays, an extra check such as a CAPTCHA after several failures, and multi-factor authentication. Say “Wrong username or password” without revealing which one was wrong.
Your turn
createLimiter({ limit, windowMs }). It returns an object with attempt(key, now), where now is a time in milliseconds. Keep the times of recent attempts for each key and forget those at or before now - windowMs. If fewer than limit remain, record the attempt and return { allowed: true, remaining } (attempts left after this one). Otherwise return { allowed: false, retryAfter }, the seconds, rounded up, until the oldest remaining attempt expires, and do not record the blocked attempt.What the checks look for
- Attempts under the limit are allowed
- The next attempt has to wait
- Old attempts expire
- Every key has its own count
- Blocked attempts do not make the wait longer
- Limits and windows can be chosen
- The pretend server blocks the sixth try
Take this with you
Count attempts per key over time, answer 429 with Retry-After, and keep people informed.
Go a little further
Instead of a fixed limit, make every failure double the waiting time: 1 second after the third failure, then 2, 4, 8 and so on.