0

Rate limiting — token bucket, leaky bucket algorithms

Intermediate5 min read·php-16-011
securitycomparelaravel-src

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:

  1. Rate limit login attempts by IP AND by username (attackers rotate IPs).
  2. Exponential backoff: 1s, 2s, 4s, 8s... delay after each failed attempt.
  3. Account lockout after N failures (with reset mechanism).
  4. CAPTCHA after N failures.
  5. 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
<?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;
}