class

KemalIdentity::RateLimiter

Inherits Reference < Object

Throttles repeated attempts against the same key.

Why the two methods are consume and reset, not check and penalise

The attempt is counted before the expensive work, not after it. Bcrypt verification is tens of milliseconds of CPU by design, which makes the login endpoint an easy denial-of-service lever — Crystal's own bcrypt documentation says so directly. A limiter that only penalised failures would have already paid for the hashing before deciding to penalise, so the lever would still work: an attacker never needs to succeed.

So consume counts and judges in one step, ahead of any lookup or hashing, and reset clears the count once someone proves they are the account holder. A failure penalises by simply not being reset.

Keys

The caller decides what to key on, and Passwords::Authenticator keys on two things at once: the login being attempted, and the source address. The login-keyed limit is what survives an attacker rotating IPs — credential stuffing is distributed by nature, so a purely address-keyed limit is close to useless against it. The address-keyed limit is what catches one host spraying many logins.

Keys arriving here are already hashed by the caller: a limiter's storage should be able to answer "is this login under attack" without retaining the login (blueprints/0007-audit-events-omit-the-login.md).

Concurrency

Implementations must be safe for concurrent use from multiple fibers on multiple threads.

Instance methods

consume(key : String) : Verdict

Counts one attempt against key and says whether it may proceed.

Called before any I/O and before any hashing. A denial must be cheap, or the limiter becomes the very lever it exists to remove.

Must not raise for a storage failure. A limiter whose Redis is unreachable returns Verdict.unavailable and lets the application's configured policy decide, because the answer differs per endpoint: a login should refuse rather than run unmetered, while a less sensitive action may prefer to stay up. An exception here would make that choice for everybody, and would surface as a 500 rather than as either policy.

Source
reset(key : String) : Nil

Clears the count for key, after a successful authentication.

Idempotent, and safe for a key that was never consumed. Must not raise, including when the store is unavailable: a reset that does not happen leaves somebody throttled slightly longer than they earned, which is not worth failing a successful login over.

Source