How to define Rate Limiters in a Laravel 11 Application
Demonstrate how to define rate limiting in Laravel 11 to improve application performance
Introduction
In this article i’ll demonstrate how to define rate limiting in Laravel 11 to enhance security and appropriate resource sharing across users of your application.
Rate Limiting
in simple terms, is a way to limit any action (especially incoming HTTP requests) within a defined window time. It is applicable to routes, IPs etc. The implementation of rate limiter is essential to shut out malicious bots, prevent DOS attacks and ensure shared access across the application users without interruption.
Laravel Rate Limiting
In Laravel 11 there’s a rate limiting abstraction which works with the cache configuration (Redis, Memcached, or database) of the application to limit any action within a defined window of time.
💡 You can watch how to define Rate Limiters in a Laravel 11 Application
The Rate limiting can be implemented directly via the facade in the boot method of the appServiceProvider
or on routes via a middleware:
- Rate Limiting Facade directly:
In Laravel 11, rate limiters can be defined within the boot
method of your application's App\Providers\AppServiceProvider
:
- Rate Limiter Middleware on the route:
Rate limiter middleware in Laravel 11 can be achieved by applying the Laravel default throttle:60,1 middleware value which indicates that it should allow 60 requests per minute before reaching the controller level:
Route::middleware('auth:api', 'throttle:60,1')->group(function () {
});
//By default the throttle is having a 1 minute window time to limit any action
The rate limiter uses the default
cache configuration of the application but you can customize
the cache driver for the rate limiter by defining a limiter key in the configuration:
'default' => env('CACHE_STORE', 'database'),
'limiter' => 'redis',
Customizing Responses for Exceeded Limits
The 429 Too Many Requests
default response can be customized when API or web requests limit is reached.
- You can return a custom response within the Rate limiter facade using the response() method
RateLimiter::for('global', function (Request $request) {
return Limit::perMinute(1000)->response(function (Request $request, array $headers) {
return response('Too many requests. Please try again later.', 429, $headers);
});
});
- You can customize the response message in the
bootstrap/app.php
by invoking the ThrottleRequestsException class inside the withExceptions() chain method of the application configuration
->withExceptions(function (Exceptions $exceptions) {
$exceptions->renderable(function (ThrottleRequestsException $e) {
return response()->json([
'message' => 'Too many requests. Please try again later.',
'retry_after' => $e->getHeaders()['Retry-After'] ?? 60,
], Response::HTTP_TOO_MANY_REQUESTS);
});
})
- You can create a 429.blade.php file in the errors directory to customize the view on a web browser for the exceeded request limit response.
//layout
@extends('master.app')
@section('title', 'Too Many Requests')
@section('content')
<div class="error-page">
<h1>429 - Too Many Requests</h1>
<p>Sorry, you have made too many requests. Please wait for a while and try again.</p>
<p>You can retry after {{ $retryAfter }} seconds.</p>
</div>
@endsection
Examples of Rate Limiter in Action
- Attaching Laravel default Rate Limiters
throttle
middleware to Route
//only 3 requests can be made in 1 minute
Route::middleware('throttle:3')->controller(PostController::class)->group(function () {
Route::get('/', 'all_posts');
});
If i head over to the browser and try to access the home page more than three times it’ll return a 429 error message.
- Custom rate limiter in the Appservice provider can also be attached to route using the middleware:
Using the for() method this global
rate limiter when applied to any routes limits the number of request to 3 perMinute:
public function boot(): void
{
RateLimiter::for('global', function ($request) {
return Limit::perMinute(3)->by($request->user()?->id ?: $request->ip());
});
}
- The
global
can be used on any route as fit for the need of the functionality of the application
Route::middleware('throttle:global')->controller(PostController::class)->group(function () {
Route::get('/', 'all_posts');
});
When i try accessing the landing page more than three times in a minute it return the Too many requests
custom message we defined earlier on.
- Returning a custom response
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(3)->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response('This will be handy for Rate limiters on API routes', 429, $headers);
});
});
- Adding the custom
api
rate limiting to the/v1/info
api route:
Route::middleware('throttle:api')->controller(PostController::class)->group(function () {
Route::get('/v1/info', function(){
return response('API rate limiting demo for version of this API');
});
});
- First Request via Insomnia returns a response
- More than three request to the api returns 429 error response
You can have multiple custom Rate limiters in the app service provide but because of the need to register other services here’s a cleaner way to keep it organize.
public function boot(): void
{
$this->configureRateLimiting();
}
protected function configureRateLimiting()
{
RateLimiter::for('global', function ($request) {
return Limit::perMinute(3)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(3)->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response('This will be handy for Rate limiters on API routes', 429, $headers);
});
});
}
Manual Interaction with the Rate Limiter Facade
To keep this article simple i’ll be doing the demo from the route callback function.
Attempt() method
The attempt() method
of the Laravel Ratelimiter Facade is used to define rate limit. It basically returns false when the callback has no remaining attempts available or return a the specified response if the limit is not reached;
The first argument accepted by the attempt
method is a rate limiter "key", which may be any string of your choosing that represents the action being rate limited, followed by the number of request per minute(window time) and callback function.
An optional fourth argument can be defined to determine how many attempts can happen within a window time in seconds
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
Route::get('/', function(){
$executed = RateLimiter::attempt(
'check-user-store:1',
2,
function() {
// Send message...
}
//fourth argument called decaySeconds in seconds,
//$decayRate = 120
);
if (!$executed) {
return 'Too many messages sent!';
}
RateLimiter::hit('check-user-store:1', 60);
});
Too many messages sent!
else increment the check-user-store:1 attempts for the next 60 seconds:tooManyAttempts() method:
The tooManyAttempts() method
can be invoked to check if a given rate limiter key has exceeded its maximum number of allowed attempts per minute:
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
Route::get('/', function(){
if (!RateLimiter::tooManyAttempts('game-status:1', 5)) {
return 'Too many attempts!';
}
return 'Attmepts not reached yet';
RateLimiter::hit('game-status:1', 60);
});
Too many attempts!
else increment the game-status:1 attempts for the next 60 seconds:remaining() method:
The remaining() method can be used to retrieve the number of attempts remaining for a given key on the rate limiter:
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
Route::get('/', function(){
if (!RateLimiter::remaining('game-status:1', 5)) {
return 'Too many attempts!';
}
RateLimiter::increment('game-status:1');
return RateLimiter::retriesLeft('game-status:1', 5);
});
increment() method:
The increment() method is invoked to increase the value of the rate limiter key which by default increase by more than one. The first argument is the key, followed by the time window and the number of increase:
RateLimiter::increment('game-status:1');
//RateLimiter::increment('game-status:1', 120, 2);
retriesLeft() method
You can use the retriesLeft() method to get the number of attempts left for a given key:
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
Route::get('/', function(){
if (!RateLimiter::remaining('game-status:1', 5)) {
return 'Too many attempts!';
}
RateLimiter::increment('game-status:1');
return RateLimiter::retriesLeft('game-status:1', 5);
});
availableIn ()
If there no more attempts left, you can use the availableIn
() method to get the number of seconds remaining until more attempts will be available.
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
if (RateLimiter::tooManyAttempts('game-status:1', $perMinute = 5)) {
$seconds = RateLimiter::availableIn('game-status:1');
return 'You may try again in ' . $seconds . ' seconds.';
}
RateLimiter::increment('game-status:1');
});
clear() method
You can invoke the clear() method of the RateLimiter facade to clear or reset the number of a rate limiter key:
use Illuminate\Support\Facades\RateLimiter;
Route::get('/', function(){
return RateLimiter::clear('game-status:1');
});
Conclusion
In this article we’ve learn about rate limiter and how to implement in Laravel. It is an essential skill to building secured Laravel applications. You can use Postman or Browser or Laravel HTTP test case to make several requests to confirm the rate restrictions. Rate limiting implementation is useful to help maintain performance, protect against abuse, and ensure fair usage across all users of the API.
Find this article useful… kindly share with your network and feel free to use the comment section for questions, answers, and contributions.