Rate Limiting and OWASP Security: Stopping Brute-Force Attacks
Rate limiting is your primary defense against brute-force attacks. Learn how to implement OWASP-compliant login security to protect your APIs from abuse.
Last month, our logs showed a spike in unauthorized login attempts targeting our /auth/login endpoint. It wasn't a massive DDoS, but a slow, distributed brute-force attack—roughly 150 attempts per minute from a rotating pool of residential proxies. Implementing robust rate limiting turned out to be the only thing standing between us and a total account takeover event.
If you’re relying solely on static passwords, you’re already behind. To align with modern OWASP security guidelines, you need a strategy that understands the difference between a legitimate user misremembering a password and a botnet trying to guess one.
Why Simple Thresholds Fail
We first tried a basic IP-based throttle. It was simple: 5 attempts per minute per IP. It broke immediately. Our office network—with 50 engineers behind a single NAT—was getting locked out every time someone had a bad Monday.
That’s the core trap of naive brute-force protection. If you block by IP, you hurt your users. If you block by username only, you’re vulnerable to distributed attacks where the attacker uses one IP per guess. You need a hybrid approach.
Implementing OWASP-Compliant Rate Limiting
To achieve real login security, your implementation needs to be account-aware and progressive. Here is the architecture we settled on:
- Global IP Rate Limit: A wide-net throttle to catch high-volume automated scanners.
- Account-Specific Thresholds: A much stricter limit on the number of failed attempts for a specific username across any IP.
- Exponential Backoff: Instead of a hard 15-minute block, we increase the wait time after every failed attempt.
If you're already using Nginx, you should master Nginx Rate Limiting: How to Protect Your API from Brute Force to handle the initial layer of defense before the request even hits your application code.
The Logic in Practice
When building this, don't just return a 429 Too Many Requests. Be helpful but vague. Return a generic error message so you don't leak whether an account exists or if the password was just wrong.
For those running Node.js or Laravel, you can simplify this logic significantly by following the patterns in Rate limiting API security: A Practical Guide for Node.js and Laravel.
| Strategy | Best Used For | Risk |
|---|---|---|
| IP-based | Volumetric attacks | False positives for NAT users |
| Username-based | Targeted brute-force | Account lockout (DoS) |
| Session-based | Credential stuffing | Easily bypassed by fresh cookies |
| Hybrid | Production APIs | Complexity to implement |
Moving Beyond Simple Throttling
Eventually, you'll find that static limits aren't enough. We moved to a "Token Bucket" approach stored in Redis, which allows for temporary "bursts" of activity while maintaining a strict long-term average. If you’re dealing with distributed systems, Redis Rate Limiting: Implementing the Token Bucket Algorithm is the gold standard for maintaining state across multiple app instances.
If you’re using Laravel, don't reinvent the wheel. I’ve written about how to handle Rate Limiting API Endpoints: Protecting Laravel Apps using the framework's native RateLimiter facade, which handles the cache-locking for you.
What I’d Do Differently
Looking back, I wasted two days trying to build a custom "reputation score" for every IP. It was overkill. The most effective API security measure was simply adding a CAPTCHA challenge after the third failed attempt on a specific account. It stopped the bots dead in their tracks without bothering our legitimate users.
Remember: rate limiting isn't a "set and forget" feature. You need to monitor your 429 error rates. If the rate is too high, you’re blocking real humans. If it’s zero, your limits are likely too permissive. Start with conservative limits and tighten them as you gather data.
FAQ
Does rate limiting prevent credential stuffing? Not entirely. Credential stuffing uses valid usernames and passwords from other breaches. You need rate limiting combined with multi-factor authentication (MFA) and breached-password detection to stop that.
Should I block the IP or the account? Always block the account temporarily for a specific user to prevent brute-forcing a specific password. Block IPs only when they exhibit malicious patterns across multiple accounts.
How long should a lockout last? Start with a short period (e.g., 5 minutes) and implement exponential backoff. If you lock an account for 24 hours, you’ve essentially enabled a Denial of Service attack for your own users.