How to define Rate Limiters in a Laravel 11 Application

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);
});
💡
What the above code means is that, if the check-user-store:1 is attempted more than 2 times return 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);
});
💡
What the above code means is that, if the game-status:1 is attempted more than 5 times return 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.

💡
Follow me on Hashnode: Alemsbaja --- X: Alemsbaja ---- Youtube: Tech with Alemsbaja to stay updated on more articles

Did you find this article valuable?

Support Alemoh Rapheal Baja by becoming a sponsor. Any amount is appreciated!