Laravel Magazine
Rate Limiting Laravel Routes and Actions the Right Way

Rate Limiting Laravel Routes and Actions the Right Way

Eric Van Johnson ·

Rate limiting protects your app from abuse, runaway loops, and expensive operations getting hammered. Laravel gives you two complementary tools: named limiters for routes and the RateLimiter facade for throttling any action you like. Here is how to use both well.

Named Route Limiters

Define limiters in a service provider, usually AppServiceProvider, using RateLimiter::for(). The name you give it becomes the argument to the throttle middleware.

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;

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

The by() call is the important part. It sets the key the limit is counted against, so authenticated users get their own 60-per-minute budget and everyone else is limited by IP address.

Attach it to routes with the throttle middleware:

Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::get('/reports', ReportController::class);
});

Dynamic Limits by Plan

Because the limiter is a closure, limits can depend on the user. This is a clean way to give paying customers higher throughput:

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->onPlan('pro')
        ? Limit::perMinute(100)->by($request->user()->id)
        : Limit::perMinute(10)->by($request->user()->id);
});

You can also return an array of limits to enforce several at once, for example a per-minute and a per-day cap:

return [
    Limit::perMinute(30)->by($request->user()->id),
    Limit::perDay(1000)->by($request->user()->id),
];

Throttling Any Action

Route middleware is not the only place limits are useful. The RateLimiter facade throttles arbitrary code. This is perfect for expensive operations like sending a verification email or calling a paid third-party API from the PHP Architect billing service.

use Illuminate\Support\Facades\RateLimiter;

$executed = RateLimiter::attempt(
    key: 'send-verification:' . $user->id,
    maxAttempts: 3,
    callback: fn () => $user->sendVerificationEmail(),
    decaySeconds: 300,
);

if (! $executed) {
    return back()->withErrors('Too many attempts. Try again in a few minutes.');
}

attempt() runs the callback only if the limit has not been hit, and returns false otherwise. For finer control you can reach for tooManyAttempts(), hit(), remaining(), and availableIn() directly.

A Couple of Gotchas

Rate limit counters live in your cache store, so in production use a shared, persistent store like Redis. The default array cache used in tests resets every request, and a per-server file cache will not coordinate limits across multiple app servers.

Also give your limiter keys a stable, unique prefix. Using something like send-verification:{id} rather than a bare {id} prevents two unrelated limiters from accidentally sharing a counter.

Done right, rate limiting is a small amount of code that saves you from a surprising number of production incidents, from brute-force login attempts to a single client accidentally DDoSing your API.

Further Reading

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.