Blog

Laravel 8 RateLimit functionality

January 8, 2022 • ☕️ 5 min read

From Laravel 8 we were introduced with RateLimit facade which brings awesome new features for route protection, actions execution, and other things where rate limiting is needed.

What do we need to start working with rate limiters?

// Use RateLimiter facade
use Illuminate\Support\Facades\RateLimiter;

Caching

Rate limiter uses cache for storing values of the first attempt and it compares its unique value with rate limit check attempts. All configuration is located in config/cache.php

By default cache driver is file, but Laravel offers us much more drivers: apc, array, database, memcached, redis, dynamodb, octane

    [
        //...
        'default' => env('CACHE_DRIVER', 'file'),
    ];

This option is also available through .env

CACHE_DRIVER=file

After changing its value through .env make sure to clean cached config values.

php artisan optimize:clear

Using for actions in controllers or some other services

    // For example
    use Acamposm\Ping\Ping;
    use Acamposm\Ping\PingCommandBuilder;

    $executed = RateLimiter::attempt(
        'pingServer='.$ip, // Unique key of this limit record
        $attempts = 2,     // Max attempts
        function($ip) {    // Closure
            
            // Ping specific ip
            $command = (new PingCommandBuilder($ip))->count(10)->packetSize(200)->ttl(128);

            // Run ping
            $ping = (new Ping($command))->run();

            // Handle response...
        },
        60 // 1 min is per default
    );

    // Check if RateLimit has allowed execution of this ping request
    if (!$executed) {
        return 'Too many pings sent!';
    }

For simpler usage RateLimit has provided us with tooManyAttempts static method which checks if we hit the request limit for a specific key in a one-minute interval.

use Illuminate\Support\Facades\RateLimiter;

// Our custom non-existing exception handler for the Limit break
use App\Exceptions\LimitException;

if (RateLimiter::tooManyAttempts('pingServer='.$ip, $attempts = 1)) {
    throw new LimitException('You reached your ping limit for one minute.');      
}

Most useful by my opinion method on RateLimit is availableIn What this method provides for us is to limit expiration. The return time value is seconds.

use Illuminate\Support\Facades\RateLimiter;
use App\Exceptions\LimitException;

if (RateLimiter::tooManyAttempts('pingServer='.$ip, $attempts = 1)) {

    // Call method
    $seconds = RateLimiter::availableIn('pingServer='.$ip);

    // Throw exception
    throw new LimitException('You reached your ping limit for one minute. Limit will expire in ' . $seconds);      
}

The last method is to clear the limit from the cache.

use Illuminate\Support\Facades\RateLimiter;

// Call clear method
RateLimiter::clear('pingServer='.$ip);

Using rate limiter with middlewares (api/web)

Rate limit also comes in handy with limiting hits on api and web endpoints. This feature is available on single routes and a group of routes.

This middleware is already loaded by default in app/Http/Kernel.php

/**
 * The application's route middleware.
 *
 * These middleware may be assigned to groups or used individually.
 *
 * @var array<string, class-string|string>
 */
protected $routeMiddleware = [
    //...
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

The easiest way to configure rate limiting is to define a specific limit rule on configureRateLimiting method from App\Providers\RouteServiceProvider

Quick example.

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

/**
 * Configure the rate limiters for the application.
 *
 * @return void
 */
protected function configureRateLimiting()
{
    RateLimiter::for('global', function (Request $request) {
        return Limit::perMinute(1000);
    });
}

Rate limiting can also be dynamic where we can set other limits for different types of users.

In the example below we use User model provided with $request, where we check if the given user is premium type. If yes, we give him unlimited uploads, if not 1 per minute.

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

/**
 * Configure the rate limiters for the application.
 *
 * @return void
 */
protected function configureRateLimiting()
{
    RateLimiter::for('uploads', function (Request $request) {
        return $request->user()->type === 'premium'
                ? Limit::none() // Set unlimited
                : Limit::perMinute(1); 
    });
}

Attaching rate limiters to routes

  • Single routes

    Route::middleware('throttle:uploads')->post('/internal/proof', [UploadController::class, 'createPost']);
  • Trough groups

    Route::middleware(['throttle:uploads'])->group(function () {
        Route::post('/audio', function () {
            //
        });
    
        Route::post('/video', function () {
            //
        });
    });

Return custom response on limit reach

Limit cache helper contains method response which can invoke a response.

We can return any supported response method. In the example below we use redirect back with errors in flash session so in views we can display error.

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

/**
 * Configure the rate limiters for the application.
 *
 * @return void
 */
protected function configureRateLimiting()
{
    RateLimiter::for('uploads', function (Request $request) {
        return Limit::perDay(1)->by($request->ip())->response(function(){
                return redirect()->back()->withErrors('limit', 'Limit reached');
            });
    });
}

Extending rate limit middleware

Why we would need to extend and override original methods? One use case is to hide X-RateLimit-Limit: x X-RateLimit-Remaining: x headers from Laravel response on the route which has a rate limit.

Firstly we would need to create a new middleware in app\Http\Midleware. We can leave the same name.

namespace App\Http\Middleware;

use Illuminate\Routing\Middleware\ThrottleRequests as OriginalThrottleRequests;

/**
...
 */
class ThrottleRequests extends OriginalThrottleRequests
{
    /**
     * @inheritdoc
     */
    protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null)
    {
        // Set empty array as headers response
        // Custom headers can be set here too
        return [];
    }
}

The last thing to make this work is to register this new middleware in app/Http/Kernel.php

/**
 * The application's route middleware.
 *
 * These middleware may be assigned to groups or used individually.
 *
 * @var array<string, class-string|string>
 */
protected $routeMiddleware = [
    //...
    //'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'throttle' => \App\Http\Middleware\ThrottleRequests::class,
    //...
];

We can still use both middlewares, but then we would need to change the name of middleware.

/**
 * The application's route middleware.
 *
 * These middleware may be assigned to groups or used individually.
 *
 * @var array<string, class-string|string>
 */
protected $routeMiddleware = [
    //...
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'throttle-noheaders' => \App\Http\Middleware\ThrottleRequests::class,
    //...
];

And then in routing files, we would need to set this new middleware.

// throttle
Route::middleware('throttle:uploads')->post('/internal/video', [UploadController::class, 'uploadVideo']);

// throttle-noheaders
Route::middleware('throttle-noheaders:uploads')->post('/internal/noheaders', [UploadController::class, 'noHeaders']);