Compound Rate Limiting vs Credential Stuffing Lab (Interactive)
Rotate 80,000 residential proxies and find the key dimension that still catches them. Compare IP-only, endpoint-plus-email compound, device-fingerprint, and step-up CAPTCHA limiters against rotating botnets while scoring NAT-campus false positives and Redis cost.
Compound Rate Limiting vs Credential Stuffing
Choose which key the limiter tracks; the botnet rotates IPs, but it cannot rotate the email it is attacking.
active key: email + IP, then challenge · threshold 5 fails → CAPTCHA
Bots blocked
94%
2,400 attempts/min leak through
Human friction
3% CAPTCHA
campus egress: 3,600 req/min on one IP
Per-dimension pressure
0.5/min per IP · 8.00/min per email · 0.5/min per fingerprint
Redis sliding-window cost
9,000 keys ≈ 0.9 MB · +0.8ms/check
Stuffing is slipping through2,400 stuffing attempts/min still reach the auth service. With 80,000 rotating IPs each address sends only 0.5 req/min — invisible under a 500 req/min IP threshold. Detection must move to the compound target-account dimension (8.0 attempts/min per email vs the 5/15min limit).
HTTP/1.1 429 Too Many Requests
Retry-After: 900
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0 · X-RateLimit-Reset: unix ts of oldest window entry
A Redis Lua script (ZREMRANGEBYSCORE + ZCARD + ZADD atomically) implements the sliding-window counter per key, so boundary bursts cannot double-spend two fixed windows. Tier the checks: shed volumetric noise at the CDN edge on IP, protect /login on endpoint+email, catch scraping on device fingerprint, and enforce business SLAs per tenant before the request ever reaches the application server.
How It Works Under the Hood
IP-only rate limiting fails twice in opposite directions: credential-stuffing botnets lease millions of rotating residential proxies so each address sends one harmless request, while a university campus hides thousands of legitimate humans behind a single CGNAT address that the same limiter happily throttles. Compound keys break the asymmetry: endpoint plus target-email can be rotated far more slowly than proxies, device fingerprints collapse headless fleets that reuse headers, and step-up invisible challenges (Turnstile proof-of-work) let real humans self-recover while bots stall. Underneath, a Redis Lua sliding-window counter (ZREMRANGEBYSCORE, ZCARD, ZADD, atomically) removes fixed-window boundary bursts at about 100 bytes per key, and every denial ships standard 429 headers with Retry-After.
Core Architectural Principles
- Per-IP, per-email, and per-fingerprint request rates are computed live against each tier threshold.
- Campus traffic behind one NAT IP measures the false-positive tax of IP-only limits in blocked humans.
- Sliding-window counters cost ~100 bytes per active key and one Redis round trip (~0.8 ms) per check.
Explain why IP-only limits fail: proxy rotation spreads abuse, NAT concentrates innocents. Then design tiers, edge IP for volume, endpoint plus target account for /login, fingerprint for scraping, tenant quota for SLA. Name the algorithm (sliding-window counter in Redis, atomic via Lua) and the contract: 429 with Retry-After, and step-up challenges before hard locks.
Richer compound keys catch smarter bots but multiply tracked keys, Redis memory, and NAT false-positive risk that step-up challenges soften.