"Rate Limiting Explained: Stopping Brute Force, Stuffing and Abuse at the Door"

Rate limiting is the least glamorous control in security and one of the few that stops attacks while they are happening. Without it, the math is entirely on the attacker's side: an unthrottled login endpoint accepts millions of password guesses per hour per connection, credential-stuffing lists circulate with billions of real username/password pairs, and every SMS/email/push you send on demand is a bill someone else decides the size of. With it, the same attacks become slow, loud and expensive enough to detect — usually before they succeed.

Yet "no rate limiting detected" is one of the most common Medium findings in scans, because the control lives in an awkward middle layer: too low-level for application code reviews to catch reliably, too high-level for network teams to own, and invisible until you specifically look for it. This guide covers what you're defending against, how to design limits that work, the response semantics that make clients behave, and how automated scanners (including ours) probe for it. The free security scan checks your endpoints for rate-limit headers and probes common login/API paths with a request burst to see whether a limiter answers.

Table of contents

  1. The attacks rate limiting stops
  2. Why per-IP limits alone fail
  3. Designing limits: windows, bursts, identities
  4. Telling clients the truth: 429 and the headers
  5. Where to implement: proxy vs app
  6. How scanners probe for rate limiting
  7. Frequently asked questions

The attacks rate limiting stops

Brute force — guessing passwords against one account. Naive, slow, and defeated by lockouts and strong passwords; still works against the admin account someone created with Summer2024! and never rotated. Rate limiting turns "millions of guesses" into "a handful per minute," which changes the economics from practical to absurd.

Credential stuffing — the modern main event. Attackers take breach dumps (billions of real email/password pairs from other sites' leaks) and replay them against your login. Nothing is "guessed" — the passwords are correct, reused by users. Success rates of 0.1–2% are enough to matter at scale, and the traffic is designed to look human: distributed sources, realistic pacing, real browser fingerprints. Rate limiting is the first structural barrier (paired with breached-password checks and MFA, which address the same attack from other angles).

Resource-abuse denial of service — not just volumetric DDoS, but the "expensive endpoint" class: search queries, report exports, password-reset mails, SMS sends, image resizing. One request may cost 100× a normal page; unthrottled, a single script with one IP can exhaust your database or your Twilio budget. The 2023–2024 wave of SMS-pumping fraud against twilio-adjacent APIs was exactly this, monetized.

Enumeration — login pages, password-reset forms and registration flows that answer differently for "unknown user" vs "wrong password" leak the user list at scale. Throttling enumeration-friendly endpoints slows discovery to a crawl (and should be paired with uniform error messages).

What rate limiting does not do: fix any of these alone. It buys time and visibility — the SLA within which your other defenses (MFA, breached-password rejection, anomaly alerts, WAF rules) operate.

Why per-IP limits alone fail

The classic implementation — count requests by remote_addr, reject above N — was designed for a friendlier internet and now fails in both directions:

False negatives (attackers pass): botnets and residential-proxy pools (the same infrastructure behind sneaker-checkout and scraping) give attackers thousands of IPs. A per-IP limit of 5/min is 7,200/day per IP; a 10,080-IP pool makes that 72 million attempts daily against your login. Attackers also stay under per-IP thresholds deliberately — stuffing at 1 request/IP/minute is a patient, quiet, effective campaign.

False positives (users suffer): CGNAT puts thousands of mobile users behind one carrier IP; corporate egress funnels a whole office through one address; university dorms, VPNs and airline Wi-Fi concentrate further. A strict per-IP limit locks out offices and campuses while the attacker rotates. (One operational note for anyone implementing this: get the real client IP first — behind nginx or a CDN you must trust only your proxy's forwarded headers, or attackers will spoof X-Forwarded-For to mint unlimited fake identities for your limiter. Our own scan infrastructure handles exactly this; the trust-model rules in our headers guide's deployment notes apply verbatim.)

The workable model is layered identity: per-IP as the cheap outer fence, plus per-account (username) limits that follow the target rather than the source, plus — where available — stronger signals: device fingerprint, session/token identity for API routes, ASN reputation. A stuffing campaign that rotates IPs still hammers usernames; per-account limits catch what per-IP cannot.

Designing limits: windows, bursts, identities

Good limiting is a handful of well-chosen budgets, not one global rule:

Tier the endpoints by blast radius. Auth endpoints (login, reset, MFA verify): tight — e.g. 5 attempts/minute per account, small burst, hard 429s. Expensive endpoints (search, export, mail send): per-user quotas (e.g. 100/day) plus per-IP sanity caps. Ordinary reads: generous — don't rate-limit your homepage into a support ticket.

Use a small burst allowance on top of the sustained rate. Real users double-click, apps retry, mobile networks blip. A token-bucket (sustained rate + bucket of N tokens) absorbs legitimate bursts while capping abuse; nginx's burst= parameter and most gateway limiters implement exactly this. A limiter with no burst punishes humans and trains your team to disable it.

Return real feedback, not silent drops. A limiter that times out connections teaches clients to retry immediately (worse load). A 429 with Retry-After teaches them to back off. See the next section — the response is half the control.

Fail closed on auth, fail open-ish elsewhere. For login/reset/MFA, a limiter outage should mean "reject" (fail closed) — the risk is account compromise. For public reads, degrade gracefully rather than taking the site down with your Redis. Decide this per tier, deliberately.

Log rejections as security events. Every 429 on an auth endpoint is a signal: source IPs, target usernames, time clustering. Aggregated, they're your intrusion-detection telemetry; discarded, they're just load you happened to shed.

Telling clients the truth: 429 and the headers

The wire protocol of rate limiting is small but load-bearing:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1695744000
  • 429 Too Many Requests — the status that says "you, specifically, slow down" (as opposed to 503, which says "I'm broken"). Legitimate clients and libraries treat it as a backoff signal.
  • Retry-After — seconds (or a date) to wait. The single most useful header for turning abusive traffic into polite traffic; retry-storms shrink dramatically when it's present.
  • X-RateLimit-* (de-facto standard) and the newer IETF RateLimit-* draft headers — advertise the budget (Limit), what's left (Remaining), when it refills (Reset). They let well-behaved API consumers self-regulate before hitting the wall, and they're what our scanner looks for as evidence that limiting exists at all.

Semantics worth knowing: 401/403 for authentication/authorization failures, 429 for volume — never use a 403 to mean "too fast," or legitimate clients' credential handling gets tangled in your throttling. And keep 429 responses cheap — no database work, no session writes — or the limiter itself becomes the DoS amplifier.

Where to implement: proxy vs app

Edge/proxy layer (nginx, Cloudflare, your gateway) — the first fence:

# nginx: per-IP login fence, 5 requests/minute sustained, burst of 3
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

server {
    location = /login {
        limit_req zone=login burst=3 nodelay;
        limit_req_status 429;
        proxy_pass http://app;
    }
}

Cloudflare: Security → WAF → Rate limiting rules — match the login path, threshold, and (on paid tiers) key by more than IP. Edge limiting is cheap, always-on, and survives app deploys — but keys only on what the proxy sees (IP, path, headers), not accounts.

Application layer — the precise fence: per-account counters (a Redis INCR with TTL per username+endpoint), per-API-key quotas, fingerprint signals. This is where "5 login attempts per account per minute regardless of source IP" lives, and where you can escalate (temporary lockout with email notification, CAPTCHA, step-up MFA). Every serious auth library (Rails' rack-attack, Laravel's RateLimiter, Django-axes, Spring Security's lockout support) ships the primitives.

The standard stack: edge limiter as the always-on coarse fence (absorbs floods cheaply), app-level per-identity limits as the fine one, uniform 429s from both. Anything less than both is where "no rate limiting detected" findings come from — the edge limiter alone misses stuffing (IPs rotate), the app layer alone drowns in floods before it can think.

One more layer people forget: the database's own limits — connection-pool caps and query timeouts. They don't replace rate limiting, but they cap the blast radius when something slips past it.

How scanners probe for rate limiting

Automated detection has to answer: is there a limiter, and would it fire in time? Our check works in two phases, and the same method works manually:

Phase 1 — listen to what the server admits. Fetch the page and inspect the full family of rate-limit headers (X-RateLimit-*, X-Rate-Limit-*, the IETF RateLimit-* drafts, Retry-After). Any of them present is evidence of a configured limiter, and the values themselves are diagnostic — X-RateLimit-Limit: 5000 on a login route says the fence is nominal.

Phase 2 — behave like an attacker and watch. Send a rapid burst (10 requests by default, policy-adjustable 3–50) at the endpoints that matter: /login, /api/v1/auth/login, /wp-login.php, /auth/login, /signin, /api/ — plus site-specific paths via the scan policy's rate_limit_paths setting, because you know your real routes better than any catalogue. A reasonable limiter (the 5/min + burst=3 kind this module's fix instructions recommend) answers 429 within the burst. No header, no 429 across ten rapid requests → the finding: Medium "No rate limiting detected", because it means brute-force, stuffing and abuse run unthrottled.

The burst sizing is deliberate: probes of 3 requests would miss every limiter configured with a burst allowance (i.e., all the well-configured ones) and drown the report in false negatives — the module's own comments document this lesson. Severity sits at Medium because absence isn't a hole by itself; it's the removal of the brake on several other attacks, and the fix is hours, not weeks.

# Manual equivalent:
for i in $(seq 1 12); do
  curl -s -o /dev/null -w "%{http_code} " -X POST https://example.com/login
done
# 200 200 200 200 200 200 200 200 200 200 200 200   → no limiter fired
# 200 200 200 429 429 ...                            → limiter works

Frequently asked questions

Won't rate limiting hurt real users?

Not if tiered: tight budgets on auth/expensive endpoints, generous on reads, small bursts everywhere. The complaints come from one global strict rule — which is a design bug, not an argument against limiting.

What rate should I start with for login?

Common, defensible starting point: 5 attempts/minute per account and per IP, burst 3, uniform error messages, escalating lockout after repeated trips (temporary, with owner notification). Tune with your logs after a week.

Is Cloudflare's rate limiting enough?

It's an excellent coarse fence, but on most plans it keys by IP and can't see accounts. Pair it with app-level per-account limits — otherwise stuffing with rotating IPs sails through under your IP threshold.

Do I still need it if I have MFA?

Yes — MFA stops success, not abuse. Throttling still protects the reset/MFA endpoints themselves (code-guessing, SMS pumping) and the budget behind every push notification.

Where should the 429s be logged?

Somewhere your security team actually looks, with source, target identity and endpoint. Auth-endpoint 429 spikes are among the earliest reliable signals of a stuffing campaign in progress.


Run the free security scan to see whether a limiter answers on your login and API paths — checked alongside TLS, headers, CORS and the rest in one pass.