Rate limiting that actually stops credential stuffing — not just a counter per IP
Why the rate limit you already have isn’t working
Almost every backend has some rate limiting. It usually counts requests per IP address and blocks anyone over the threshold. Against a single abusive script that works fine. Against a real credential-stuffing campaign it does nothing, because the attacker knows exactly how you count and is built to defeat it: the login attempts come from thousands of residential IPs, each one staying comfortably under your per-IP ceiling. No single address looks abusive. The aggregate is drowning your login endpoint in stolen-password guesses.
Credential stuffing is not a volume problem you can solve with a bigger counter. It is a distribution problem. Stopping it means recognizing the pattern — the same endpoint, the same shape of request, the same failure signature — regardless of how many addresses it is smeared across.
Counting has to survive being spread out
The first requirement is a rate limit that works across the whole edge, not per worker and not per IP in isolation. Smart WAF keeps its hot counters in a shared-memory tier for nanosecond access on the request path, backed by a distributed counter tier so that a client seen once on one node is known everywhere. That is the ordering that keeps it fast: memory first, distributed store as the shared source of truth — never a database call in the request path.
On top of raw counting, the limit is keyed by what matters. A limit per credential being tried, per endpoint, and per behavioral fingerprint catches the campaign that a per-IP limit misses, because the attacker cannot spread the target across many IPs the way it spreads the source.
Recognize the campaign, not just the request
Volume is only half the signal. A credential-stuffing run has a behavioral shape: a high failed-login ratio, requests that skip the pages a human would load first, timing that is too regular to be a person, and reuse of the same automation fingerprint across otherwise unrelated addresses. Our ML entity scoring aggregates that behavior across a client’s history, so a slow, distributed attack that never trips a simple threshold still surfaces as one coordinated entity.
That scoring is what lets the response be proportionate instead of blunt.
Graduated response beats a hard block
A hard IP block is a bad tool for this. It punishes the innocent user behind a shared corporate NAT and it barely inconveniences the attacker, who just rotates to the next address. So the response climbs the challenge ladder instead:
- A suspicious client meets proof-of-work first — cheap for a real browser loading a login page once, expensive for a script firing thousands of attempts.
- Persistent automation gets a CAPTCHA, which a human clears and a headless bot stalls on.
- Only confirmed, sustained malice gets dropped outright — and volumetric floods are shed at L3/L4 in the kernel before they reach the WAF at all.
The real user behind the shared IP sees, at worst, a one-time challenge. The botnet pays a compounding cost on every single attempt until the campaign is no longer economical.
What we don’t claim
Rate limiting and challenges raise the cost of an attack; they do not make your accounts unbreakable. If a password has already leaked and the user reused it, the honest fix is multi-factor authentication in your application — no edge control substitutes for that. What the edge does is make large-scale automated guessing uneconomical and keep the flood off your origin, all inside our ≤5 ms design budget for normal traffic.
If your login endpoint is taking sustained abuse, tell us your domain and we will tune the limits with you.