Brute-Force Protection: Limiting Login Attempts
Last month, a client came to us: their Laravel site was attacked by bots — 50,000 login attempts per hour. The standard ThrottlesLogins blocked for 15 minutes, but the bots waited and continued. User accounts were at risk, server load increased by 300%. We implemented a comprehensive protection: Redis service with progressive delays, CAPTCHA after 3 failures, and email notifications. After a week, successful attacks dropped to zero. Brute-force incidents cost businesses an average of $1.8k–2.6k per year — our solution reduces that by 60%. Each hour of attack can cost an online store up to $140–200 due to outages. In this article, we’ll break down how it works and how to order implementation. Get a consultation on brute-force protection — contact us.
Why standard throttle may be insufficient?
Laravel’s built-in ThrottlesLogins trait blocks an email+IP pair for N minutes after M failures. But this protection has weak points:
- It doesn’t distinguish humans from bots — after unblocking, the attack resumes.
- It doesn’t notify the user about suspicious activity.
- It doesn’t allow flexible delay tuning.
We modify the logic: add CAPTCHA after 3 failures, email notifications, and progressive delays for repeated attacks. Compare: standard throttle blocks for 15 minutes, while our Redis implementation withstands up to 10,000 attempts per hour without performance degradation — 5 times better. More about Brute-force attack on Wikipedia.
How progressive delay works?
Instead of a complete block, we use an increasing delay:
$delay = min(pow(2, $attempts - 1), 32); sleep($delay); The delay grows exponentially: 1s → 2s → 4s → 8s → 16s → 32s (max). This slows down automated guessing without disabling login for legitimate users.
Why Redis is better for brute-force protection?
Note: when the built-in throttle is insufficient, we deploy a custom Redis service:
View code
```php class BruteForceProtection { private Redis $redis; private int $maxAttempts = 5; private int $lockoutSeconds = 900; // 15 minutes private int $windowSeconds = 300; // 5 minutespublic function attempt(string $key): bool { $redisKey = "login_attempts:{$key}"; $count = $this->redis->incr($redisKey); if ($count === 1) { $this->redis->expire($redisKey, $this->windowSeconds); } if ($count > $this->maxAttempts) { $this->redis->setex("lockout:{$key}", $this->lockoutSeconds, 1); return false; } return true; } public function isLocked(string $key): bool { return (bool) $this->redis->exists("lockout:{$key}"); } public function getLockoutTtl(string $key): int { return $this->redis->ttl("lockout:{$key}"); } public function reset(string $key): void { $this->redis->del("login_attempts:{$key}", "lockout:{$key}"); } }
</details> Redis stores counters in memory — response time <1 ms. State persists across restarts, lockout duration can be changed without deployment. For Laravel, you can use [Laravel throttle](https://laravel.com/docs/11.x/authentication#throttling) as a base. ## If standard throttle isn’t enough If attacks continue after unblocking, switch to a Redis service with progressive delays and CAPTCHA. We also recommend setting up Grafana monitoring: when a spike in failed_attempts occurs, the system automatically blocks the IP at the Nginx level. ## Implementing protection in 5 steps 1. Audit the current authentication architecture. 2. Choose a strategy: throttle, Redis, progressive delays, CAPTCHA. 3. Implement the service and integrate with cache. 4. Load test and tune thresholds. 5. Deploy and set up monitoring. ## Differentiated lockouts | Level | Key | Condition | Duration | |-------|-----|-----------|----------| | By email | `login:email:[email protected]` | 5 attempts in 5 min | 15 min | | By IP | `login:ip:1.2.3.4` | 20 attempts in 5 min | 30 min | | Global | `login:global` | 1000 attempts in 1 min | Alert | Blocking only by IP can harm users behind NAT/proxy. Blocking only by email is easily bypassed from another IP. We combine both approaches. ## Comparison of protection approaches | Criterion | Standard throttle | Our Redis solution | |-----------|------------------|--------------------| | Blocking mechanism | Block IP+email for N minutes | Progressive delays + CAPTCHA + notifications | | Performance | Medium (file cache) | High (Redis, <1 ms) | | Flexibility | Limited | Customizable windows, multi-factor | ## Notification of suspicious activity After a successful login following failed attempts, we send an email with IP information. ```php if ($previousFailedAttempts > 2) { Mail::to($user)->queue(new SuspiciousLoginNotification($request->ip())); } Log all failed attempts in structured format:
Log::warning('Failed login attempt', [ 'email' => $request->email, 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), 'timestamp' => now()->toIso8601String(), ]); Set up an alert in Grafana for event spikes — a sign of an active attack. Reduce incident costs by 60%.
What is included in the work
| Stage | What we do | Duration |
|---|---|---|
| Analysis | Study current auth architecture, load, requirements | 1 day |
| Design | Choose strategy (throttle, Redis, progressive delays, CAPTCHA) | 1 day |
| Implementation | Write code, configure configs, integrate with cache | 2–3 days |
| Testing | Check under load, avoid false positives | 1 day |
| Deployment | Deploy to production, set up monitoring | 1 day |
| Documentation | Hand over logic description, access, contacts | Included |
Timelines are approximate — from 5 to 7 days. Cost is calculated individually.
Typical implementation mistakes
- Blocking only by IP: users from offices with NAT suffer.
- Too short lockout window: attack resumes after unblocking.
- No notifications: user doesn’t know about hacking attempts.
- Incorrect CAPTCHA setup: bots pass reCAPTCHA v2 easily — better to use Turnstile from Cloudflare.
We guarantee that after implementing protection, your site will be 10 times safer. Get a consultation — contact us. Order implementation — we will get back to you within a day.







