Rate limiting — token bucket, leaky bucket algorithms
Concept
Rate limiting prevents brute force attacks, credential stuffing, and API abuse by restricting how many requests a client can make in a given time window.
Laravel's RateLimiter facade: Provides named rate limiters with flexible configuration. Used by the throttle middleware and directly in code.
The throttle middleware: throttle:60,1 — 60 requests per 1 minute. throttle:api — uses a named rate limiter defined in AppServiceProvider or RouteServiceProvider. Responds with HTTP 429 when exceeded. Adds X-RateLimit-Limit and X-RateLimit-Remaining headers.
Key-based rate limiting: Rate limits are keyed per user, IP, or any combination. RateLimiter::for('login', fn($request) => Limit::perMinute(5)->by($request->ip())) — 5 login attempts per IP per minute.
Brute force protection patterns:
- Rate limit login attempts by IP AND by username (attackers rotate IPs).
- Exponential backoff: 1s, 2s, 4s, 8s... delay after each failed attempt.
- Account lockout after N failures (with reset mechanism).
- CAPTCHA after N failures.
- Notify users of suspicious login attempts.
Cache::lock(): For distributed rate limiting or atomic operations (prevents race conditions in high-concurrency scenarios).
Code Example
<?php
// In RouteServiceProvider::boot() or AppServiceProvider::boot()
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function(\Illuminate\Http\Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function(\Illuminate\Http\Request $request) {
return [
Limit::perMinute(5)->by($request->ip()), // 5/min per IP
Limit::perMinute(10)->by($request->input('email')), // 10/min per email
];
});
// Apply in routes
Route::post('/api/resource', handler(...))->middleware('throttle:api');
Route::post('/login', LoginController::class)->middleware('throttle:login');
// Manual rate limiting in code
use Illuminate\Support\Facades\RateLimiter;
function attemptLogin(string $email, string $password): mixed
{
$key = 'login:' . $email . ':' . request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
throw new \TooManyAttemptsException("Too many attempts. Retry in $seconds seconds.");
}
$user = auth()->attempt(['email' => $email, 'password' => $password]);
if ($user) {
RateLimiter::clear($key); // reset on success
return $user;
}
RateLimiter::hit($key, 300); // count failure, 5 minute window
return false;
}
// Exponential backoff — penalize repeated failures
function loginWithBackoff(string $email, string $password): mixed
{
$failures = Cache::get("failures:$email", 0);
$delay = min(pow(2, $failures), 300); // 1, 2, 4, 8, ... up to 300s
if ($delay > 0 && Cache::has("locked:$email")) {
$remaining = Cache::ttl("locked:$email");
throw new \RuntimeException("Account locked for $remaining more seconds");
}
$user = auth()->attempt(['email' => $email, 'password' => $password]);
if (!$user) {
$failures++;
Cache::put("failures:$email", $failures, 3600);
Cache::put("locked:$email", true, $delay);
} else {
Cache::forget("failures:$email");
}
return $user;
}