Password Reset Throttle

MediumdesignAlso known as Sliding Window Log Rate Limiter

A bank's "forgot password" button sends an email with a reset link. Attackers use it to flood someone's inbox, so each email address gets at most 3 reset emails in any rolling hour. "Any rolling hour" is strict: unlike a per-clock-hour counter, nobody can send 3 emails at 9:59 and 3 more at 10:00. When the button is blocked, the page says how long to wait.

This is the sliding-window log from Rate Limiting: it stores the time of every allowed request in the window, which costs more memory than a counter but is exact. It suits low limits on sensitive actions such as logins, sign-ups and reset emails, and the wait time becomes the Retry-After header of an HTTP 429 response.

Your task

Write a class ResetThrottle:

  • constructor(limit, windowSec).
  • request(email, t): a reset request at time t (whole seconds). Return true and record it if the address has had fewer than limit allowed requests in the window; otherwise return false and record nothing.
  • waitTime(email, t): return how many seconds from t until a request for this address would be allowed. Return 0 if one would be allowed at t. This does not record a request.

The window at time t covers the times greater than t − windowSec and up to t. So a request made at time s stops counting at exactly s + windowSec. Each address is limited separately.

class ResetThrottle

Examples

Example 1
Input
new ResetThrottle(3, 3600)
.request("kai@mail.test", 0)
.request("kai@mail.test", 600)
.request("kai@mail.test", 1200)
.request("kai@mail.test", 1800)
.waitTime("kai@mail.test", 1800)
.request("kai@mail.test", 3599)
.request("kai@mail.test", 3600)
.waitTime("kai@mail.test", 3600)
Output
[null, true, true, true, false, 1800, false, true, 600]
Why

Requests at 0, 600 and 1,200 fill the limit of 3. At 1,800 the request is refused, and the oldest one (at 0) stops counting at 0 + 3,600 = 3,600, so the wait is 3,600 − 1,800 = 1,800 seconds. At 3,599 it still counts. At 3,600 it has left the window, so the request is allowed. Now the window holds 600, 1,200 and 3,600, and the next slot opens when 600 leaves at 4,200: a wait of 600.

Example 2
Input
new ResetThrottle(2, 60)
.request("mo@mail.test", 58)
.request("mo@mail.test", 59)
.request("mo@mail.test", 60)
.request("mo@mail.test", 61)
.request("li@mail.test", 61)
Output
[null, true, true, false, false, true]
Why

A fixed per-minute counter would reset at 60 and allow 4 requests between 58 and 61. Here the window at 60 still contains 58 and 59, so 60 and 61 are refused; the limit of 2 holds in every 60-second span. Another address has its own log.

Constraints

  • 1 ≤ limit ≤ 1,000
  • 1 ≤ windowSec ≤ 1,000,000
  • 0 ≤ t ≤ 1,000,000,000, and t never decreases from one call to the next
  • email is a non-empty string of up to 40 characters
  • At most 3,000 calls per test
Start with brute force

Store every allowed request forever and, on each call, count the stored times inside the window. With 3,000 calls that is up to about 4.5 million comparisons, and the memory grows with the whole history.

Expired times can never count again, so dropping them from the front of a queue keeps at most limit times per address and makes each call cost O(1) on average.

Hints

0 of 4 shown

Walkthrough and solution

Unlocks when you pass every test. Stuck for a while? Looking is fine too, as long as you then close it and write the code yourself.

Ctrl+Enter runs

Run your code to see results here. Run checks the examples. Submit checks every test, including hidden edge cases.

Write code