Rate limiting caps how many requests a user or IP address can make in a time window, so password guessers, scrapers and buggy scripts get a polite βslow downβ (429) instead of your server's full attention.
π’It's the ride attendant at a theme park: everyone gets a turn, but nobody can jump back in line twenty times a minute. Wait your turn, and you're welcome back.
Not every visitor plays fair
Stops password guessing
Five tries a minute turns a million-guess attack from minutes into years.
Protects your server
One runaway script can't use up all your CPU, database connections or email quota.
Fair for everyone
API users share capacity fairly, and you can give paying customers higher limits.
Brute force without a limit
A counter in front of your route
Add rate limits to your app
-
Quick limit with
throttle:max,minutesThe fastest way to limit a route is the
throttlemiddleware with two numbers: how many requests, per how many minutes.Route::post('/contact', [ContactController::class, 'send']) ->middleware('throttle:3,1'); // 3 requests per 1 minuteThe fourth submission within a minute gets a 429 Too Many Requests. Logged-in users are counted by user ID; guests by IP address.
-
Define named limiters with
RateLimiter::forFor anything more than a one-off, define a named limiter in
AppServiceProvider. It's a closure that receives the request and returns aLimit.use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; public function boot(): void { RateLimiter::for('comments', function (Request $request) { return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); }->by(...)sets the key. Other windows includeLimit::perSecond(),perHour()andperDay(). -
Attach a named limiter with
throttle:nameRoute::post('/posts/{post}/comments', [CommentController::class, 'store']) ->middleware(['auth', 'throttle:comments']);Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () { Route::apiResource('posts', Api\PostController::class); });API responses also include
X-RateLimit-LimitandX-RateLimit-Remainingheaders so well-behaved clients can pace themselves. -
Smarter limits: per plan, multiple limits, custom responses
Because the limiter is just PHP, you can return different limits for different users, or several limits at once.
RateLimiter::for('uploads', function (Request $request) { if ($request->user()?->isPro()) { return Limit::none(); } return [ Limit::perMinute(10)->by($request->user()?->id ?: $request->ip()), Limit::perDay(100)->by($request->user()?->id ?: $request->ip()), ]; });Add
->response(fn () => response('Slow down!', 429))to aLimitto customise what blocked clients see. -
Throttle logins (already done for you)
The login code in Laravel's starter kits (Breeze and the newer kits) and in Fortify already limits failed attempts: 5 tries, keyed by email + IP, then a lockout with a "Too many login attempts. Please try again in N seconds." message. Here's the pattern, so you can use it in your own code:
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; $key = Str::transliterate(Str::lower($this->input('email')) . '|' . $this->ip()); if (RateLimiter::tooManyAttempts($key, 5)) { $seconds = RateLimiter::availableIn($key); throw ValidationException::withMessages([ 'email' => "Too many login attempts. Try again in {$seconds} seconds.", ]); } if (! Auth::attempt($this->only('email', 'password'))) { RateLimiter::hit($key); // count the failure (60s decay) throw ValidationException::withMessages(['email' => __('auth.failed')]); } RateLimiter::clear($key); // success: reset the counterβΉοΈ Why email + IP?Keying only by IP lets an attacker try one password against many accounts. Keying only by email lets anyone lock a real user out. Combining both limits guessing without making lockouts easy to abuse.
Avoid this, do that
Route::post('/password/email', ...);
Route::post('/register', ...);
// no limits at all
Endpoints that send email or create accounts are favourite targets for spam and abuse.
Route::post('/password/email', ...)
->middleware('throttle:6,1');
Limit anything that sends messages, creates records or checks secrets.
Limit::perMinute(5)->by('global');
A single shared key means one noisy client blocks everyone.
Limit::perMinute(5)
->by($request->user()?->id ?: $request->ip());
Key per user (or per IP for guests) so limits apply to each client separately.
Rate limit counters live in your default cache store. If you run several servers, use Redis or the database cache so every server sees the same counts. The file store only counts per machine.
- Add
throttle:3,1to a test route and refresh it four times quickly. Note the 429 page. - Define a
commentslimiter inAppServiceProvider(5 per minute per user) and attach it to your comment route. - Hit an API route with
curl -iand find theX-RateLimit-Remainingheader. - With a starter kit installed, enter a wrong password six times and read the lockout message.
Key takeaways
- Rate limiting caps requests per key (user or IP) per time window; blocked clients get 429.
throttle:5,1is the quick version;RateLimiter::for('name', ...)inAppServiceProvideris the flexible one.- Attach named limiters with
throttle:nameon routes or groups. - Starter kits already throttle logins by email + IP using
RateLimiter::hitandtooManyAttempts. - Limit anything that sends email, creates accounts or checks secrets.