There are some kind of attacks to web applications where the user can send thousand of requests per second which can exhaust the server resources. For this Laravel provides us the rate limiting feature.
Laravel rate limiting is a powerful feature introduced since Laravel 8.0. This feature allow us to limit number of requests sent to the application for specified amount of time.
To better understand what is the rate limiting for, i have worked on a project recently that requires integration with an SMS third party API for OTP and verification purposes. So we created an API in laravel that takes the user phone and send an SMS message. However we discovered that someone else using the SMS API and using some tool that send sms messages to dummy numbers and the SMS package becomes empty.
So we thinking about a way to solve such problem and we come to a solution to limit number of requests per user to send just one sms message per minute by utilizing the Rate Limiting functionality in Laravel.
To implement Rate Limiting there are two ways:
- The first method globally through the AppServiceProvider and apply it to routes using throttle middleware.
- The second method is to specify it in controller actions.
Let’s start with the first method, to define Rate Limiting you can do this through the AppServiceProvider in the boot() method:
app/Providers/AppServiceProvider.php
<?php namespace App\Providers; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. */ public function boot(): void { RateLimiter::for("sms", function (Request $request) { return Limit::perMinute(1); }); } }
In this code we are using RateLimiter facade which gives us the for() method. The for() method accept the name of the limiter which we will use later to apply to routes and a callback that return the limit configurations.
Limit configurations defined using the Illuminate\Cache\RateLimiting\Limit class. This class contains many methods to build the configurations.
In this example we define a rate limiter called “sms” that limit requests to any route that use this limiter to one request per minute.
There are also other methods available in the Limit class like Limit::perSecond(), Limit::perHour(), Limit::perDay() etc.
The next step is to apply the limiter to the appropriate routes using limiter name:
routes/web.php
Route::middleware(['throttle:sms'])->group(function () { Route::get('send_sms/{phone}', function ($phone) { // Actual implementation details hidden, just we output a text message. return "SMS sent to: " . $phone; }); });
As shown in this code we wrapped the routes that need rate limited with the throttle middlware specifying “sms” as the limiter name that we defined above in the boot() method.
When running this code and navigating to /send_sms route, you will see the text ouput on the browser screen. However if you refresh the browser again you will see “429 Too Many Requests” error as you have to one minute so you can send another request.
You can fine tune and define as many rate limiters as needed depending on your application:
public function boot(): void { RateLimiter::for("global", function (Request $request) { return Limit::perMinute(5000); }); RateLimiter::for("sms", function (Request $request) { return Limit::perMinute(1); }); RateLimiter::for("downloads", function (Request $request) { return Limit::perMinute(10); }); RateLimiter::for("emails", function (Request $request) { return Limit::perMinute(5); }); }
routes/web.php
Route::middleware(['throttle:global'])->group(function () { Route::get('/', fn () => view('welcome')); Route::get('news', fn() => view('news')); Route::get('download', fn() => view('download'))->middleware('throttle:downloads'); Route::middleware(['throttle:sms'])->group(function () { Route::get('send_sms/{phone}', function ($phone) { return "SMS sent to: " . $phone; }); }); });
Here we defined other limiters like global, downloads, emails. The global limiter allows only 5000 requests per minute, this applied for all the application. The downloads limiter for file downloads and the emails limiter for sending email messages and only restricted to 5 requests per minute.
Rate Limiting Segmentation
The above SMS rate limiter example not fully functional as we defined it to allow one request per minute for all users which is not what we need. However Laravel rate limiter gives us the ability to segment rate limits per user using Limit::by() method like so:
RateLimiter::for("sms", function (Request $request) { return Limit::perMinute(1) ->by($request->user()->id); });
I invoked the by() method providing the current logged in user id using $request()->user()->id. This tells the rate limiter to allow one request per minute for the current authenticated user.
What about if the user is not authenticated, how to differentiate the current user. Typically using the user IP address like so:
RateLimiter::for("sms", function (Request $request) { return Limit::perMinute(1) ->by($request->user()?->id ?: $request->ip()); });
In this code we added a check for the current user id which have to exist and not null meaning that the user is authenticated otherwise we use the user IP address. Now this rate limiter now becomes more functional.
Custom Condition in Rate Limiter
You can build custom logic inside of the rate limiter so to decide whether to limit or not limit depending on some condition. As an example to you can limit requests for all users except the admin user:
RateLimiter::for('global', function (Request $request) { return $request->user()->is_admin ? Limit::none() : Limit::perMinute(5000); });
Here if the user is_admin flag is not true, the rate limiter will be applied otherwise it will not be applied.
Using Rate Limiter in Controller Actions
The second method of using rate limiter is inside controller action directly using RateLimiter::attempt() method:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; class VideoController extends Controller { public function watch(Request $request) { $executed = RateLimiter::attempt('watch:'. $request->ip(), 20, function () { echo "Currently streaming"; }); if(!$executed) { return "Exceeded watch limit"; } } }
The attempt() method accepts a limiter key as the first argument. The convention is to differentiate the key using the current user as in this example i used to $request->ip() for the user IP. Or you can use the user id for authenticated users like $request->user()->id.
The second argument is the max attempts per minute which in this example is 20 attempt. The third argument is a callback with the actual code to execute. If the user exceeded the max attempts it will exit and proceed to the next line with “Exceeded watch limit” text.
Yo can check if the user makes too many attempts using RateLimiter::tooManyAttempts() method:
$key = 'watch:'. $request->ip(); if (RateLimiter::tooManyAttempts($key, $perMinute = 20)) { $seconds = RateLimiter::availableIn($key); return 'You may try again in '.$seconds.' seconds.'; } $executed = RateLimiter::attempt($key, 20, function () { echo "Currently streaming"; });
like the attempt() method the tooManyAttempts() accepts the key and max attempts and return true if the user exceeded the max attempts for the specified key. The availableIn() return the number of seconds remaining until more attempts will be available.
The RateLimiter::remaining() method return the number of remaining attempts for the specified key. Also you can increment the limiter manually using the RateLimiter::increment() method which takes the limiter key and increment it by 1.
if (RateLimiter::remaining($key, $perMinute = 20) == 1) { RateLimiter::increment($key); echo "Incrementing max attempts ... "; }