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
filedriver works locally but does not scale across multiple servers. - Include the
Retry-Afterheader in your responses. Well-behaved clients will back off and retry after the indicated delay.