Laravel Rate Limiting Reference Guide

A Laravel rate limiting reference guide: throttling requests with the throttle middleware and named rate limiters.

The throttle middleware

The quickest way to rate limit a route is the built-in throttle middleware, which takes a maximum attempt count and a decay window in minutes:

Route::get('/search', [SearchController::class, 'results'])
    ->middleware('throttle:30,1'); // 30 requests per minute

By default it buckets attempts by the authenticated user's ID, falling back to IP address for guests, so one user hammering an endpoint doesn't exhaust another user's allowance. Once the limit is hit, Laravel returns a 429 Too Many Requests response automatically — no extra code needed — along with Retry-After and X-RateLimit-* headers telling the client how long to wait.

Named rate limiters

For anything beyond a flat request count, define a named limiter with RateLimiter::for(), typically in bootstrap/app.php or a service provider's boot() method:

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

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

Reference it on a route the same way as the built-in limiter, but by name instead of a raw count:

Route::get('/api/search-suggest', [SearchController::class, 'suggest'])
    ->middleware('throttle:search');

Named limiters are worth the extra step whenever different routes need different rules for the same underlying concern — a typeahead endpoint hit on every keystroke usually wants a much higher ceiling than the page it feeds into, and a named limiter is the natural place to express that rather than repeating raw counts across routes.

Varying the limit by condition

Because the limiter callback receives the request, it can return a different Limit depending on who's asking:

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

A callback can also return an array of limits — useful for stacking a short burst allowance with a longer sustained one, such as [Limit::perMinute(10), Limit::perDay(1000)], where a request only needs to clear one to be rejected.

Segmenting by more than the user

by() accepts any string, so a limiter can key on something other than user/IP — a route parameter, for example, to cap actions per resource rather than per caller:

RateLimiter::for('comments', function (Request $request) {
    return Limit::perMinute(5)->by($request->route('post')->id);
});

Customizing the response

response() on a Limit overrides the default 429 JSON/HTML response — handy for an API that wants a consistent error envelope:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->response(function (Request $request, array $headers) {
        return response()->json(['message' => 'Slow down.'], 429, $headers);
    });
});

Rate limiting outside HTTP

The same RateLimiter facade works anywhere in the app, not just behind middleware — useful for throttling something like login attempts or a queued job's calls to a third-party API:

$key = 'send-sms:' . $user->id;

if (RateLimiter::tooManyAttempts($key, 5)) {
    $seconds = RateLimiter::availableIn($key);
    throw new TooManyRequestsException("Try again in {$seconds} seconds.");
}

RateLimiter::hit($key, $decaySeconds = 60);

Testing rate limits

Feature tests exercise the throttle middleware automatically, so asserting the 30th request in a burst succeeds and the 31st returns 429 is enough to lock in the behavior. RateLimiter::clear($key) resets a limiter's count between tests, and withoutMiddleware('throttle') skips throttling entirely for tests that aren't about the limit itself.