paulund

Rate Limiting

Rate Limiting

Rate limiting protects your application from abuse — whether that is brute-force login attempts, API flooding, or scraping. Laravel ships with a flexible rate limiter that you can apply to routes, middleware, or any arbitrary code path.

The Throttle Middleware

The simplest way to rate-limit a route is with the throttle middleware. The argument is attempts:minutes:

Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1'); // 5 attempts per 1 minute

This applies a per-IP rate limit. After five failed attempts in one minute, Laravel returns a 429 Too Many Requests response and includes a Retry-After header.

Named Rate Limiters

For more control, define named rate limiters in your RouteServiceProvider (or AppServiceProvider in Laravel 11+):

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        RateLimiter::define('api', function (Request $request) {
            return $request->user()
                ? Limit::perMinute(60)->by($request->user()->id)
                : Limit::perMinute(20)->by($request->ip());
        });
    }
}

Apply the named limiter to routes:

Route::middleware('throttle:api')->group(function () {
    Route::resource('articles', ArticleController::class);
});

Authenticated users get 60 requests per minute; unauthenticated users get 20. The by() method sets the bucket key, so each user or IP has its own counter.

Multiple Limits

You can stack limits to allow short bursts while enforcing a longer-term cap:

RateLimiter::define('uploads', function (Request $request) {
    return [
        Limit::perMinute(3),           // No more than 3 uploads per minute
        Limit::perHour(20)->by($request->ip()), // No more than 20 per hour
    ];
});

Using the Rate Limiter Programmatically

You are not restricted to middleware. The RateLimiter facade lets you check and hit limits anywhere in your code:

use Illuminate\Support\Facades\RateLimiter;

$key = 'password-reset:' . $request->ip();

if (RateLimiter::too_many_attempts($key, 3)) {
    $seconds = RateLimiter::availableIn($key);
    return response()->json([
        'message' => "Too many attempts. Please wait {$seconds} seconds.",
    ], 429);
}

// Attempt the operation
if (!$this->resetPassword($request)) {
    RateLimiter::hit($key, 60); // Increment counter, 60-second decay
    return response()->json(['message' => 'Invalid token.'], 422);
}

// Success — clear the limiter
RateLimiter::clear($key);

Customising the Response

By default, Laravel returns a generic 429 response. You can customise it by catching the TooManyRequestsException:

// In app/Exceptions/Handler.php
use Illuminate\Http\Exceptions\TooManyRequestsException;

public function register(\Illuminate\Foundation\Exceptions\Handler $e): void
{
    $this->reportable(function (TooManyRequestsException $e) {
        // Log or notify if needed
    });

    $this->renderable(function (TooManyRequestsException $e, \Illuminate\Http\Request $request) {
        return response()->json([
            'error'       => 'Rate limit exceeded.',
            'retry_after' => $e->retryAfter,
        ], 429);
    });
}

Tips

  • Rate limit login and password-reset endpoints as a minimum. These are the most commonly targeted routes.
  • Use per-user limits on authenticated routes rather than per-IP limits. A single office IP should not cause all users in the building to be locked out.
  • Set the cache driver to Redis or Memcached in production. The default file driver works locally but does not scale across multiple servers.
  • Include the Retry-After header in your responses. Well-behaved clients will back off and retry after the indicated delay.