SkillAgentSearch skills...

Ratelimiter

A leaky bucket rate limiter and corresponding middleware with route-level granularity compatible with Laravel.

Install / Use

/learn @artisansdk/Ratelimiter
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Rate Limiter

A leaky bucket rate limiter and corresponding middleware with route-level granularity compatible with Laravel.

Table of Contents

Installation

The package installs into a PHP application like any other PHP package:

composer require artisansdk/ratelimiter

Once installed, you will need to bind your choice of Bucket implementations for the rate Limiter class. Choose either the Leaky or the Leaky Evented bucket if you need additional event dispatching. Add the following lines to your App\Providers\AppServiceProvider:

use ArtisanSdk\RateLimiter\Buckets\Leaky;
use ArtisanSdk\RateLimiter\Contracts\Bucket;

public function register()
{
    $this->app->bind(Bucket::class, Leaky::class);
}

If you do plan on using the Evented leaky bucket then you'll also want to change to the following binding to your register() method. The event dispatcher is injected automatically by Laravel:

use ArtisanSdk\RateLimiter\Buckets\Evented;
use ArtisanSdk\RateLimiter\Contracts\Bucket;

public function register()
{
    $this->app->bind(Bucket::class, Evented::class);
}

The package includes middleware for the rate limiter which is compatible with Laravel's built in Illuminate\Routing\Middleware\ThrottleRequests. Simply update the App\Http\Kernel::$routeMiddleware array so that the throttle key points to ArtisanSdk\RateLimiter\Middleware like so:

protected $routeMiddleware = [
    // ...
    'throttle' => \ArtisanSdk\RateLimiter\Middleware::class,
];

Now requests will go through the leaky bucket rate limiter. The requests are throttled according to the algorithm which leaks at rate of 1 request per second (r\s) with a maximum capacity of 60 requests with a 1 minute timeout when the limit is exceeded. This is based on the default Laravel signature of throttle:60,1 which is found in App\Http\Kernel::$middlewareGroups under the api group:

protected $middlewareGroups = [
    // ...
    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

Change the rates or add throttle:60,1 to the web group as well to rate limit even regular page requests. See the Usage Guide for more options including using the rate limiter and bucket without the middleware.

Usage Guide

Overview of the Laravel Rate Limiter

Laravel shipped without rate limiting for years and so it is a welcomed addition. To be fair, some rate limiting is better than none at all. From a security perspective though, at best Laravel's rate limiter only slows down a hacker, typically trips up legitimate usage, and presents a false sense of security.

Laravel's Implementation

Laravel has a fixed decay rate limiter. With default settings of throttle:60,1 this means that a client could make 60 requests with 1 minute of decay before hitting a 1 minute forced decay timeout. The client could make 60 requests in 1 second or distributed over 60 seconds at a rate of 1 request per second (1 r/s). If the requests are evenly spaced at about 1 r/s then the client will not be rate limited. This means that throttle:120,2 is effectively the same as 1 r/s but tracked for 2 minutes and allowing a larger burst limit up to 120 requests. Meanwhile throttle:120,1 would be an effective rate of 2 r/s with the same burst limit.

Problem 1: Bursting Exploit

Generally you want both of these numbers to be large because that provides more tracking of abuse while allowing for sufficient legitimate requests. For example if the goal is to get 1 r/s average over 24 hours up to 100K requests that would translate to throttle:100000,1440. Every day a client could dump 100K requests in 1 second! So much for 1 r/s load balancing. Furthermore, there's no penalty for abuse – just wait the 1440 minutes and you can do it again as if you made 1 r/s non stop for ever. So you lower it to throttle:3600,60 so bursting is limited to 3600 requests but the rates are reset every hour. There might be a sweet spot but it is hard to get just right.

Problem 2: No Granularity

Also the signature for the client is determined by the domain and IP address of the requester. While most hackers will randomize their IP and almost all rate limiters suffer from this (and the shared IP address issue common on public networks), all requests made by a user dump into the same cache of hits for that client. So you set your throttler differently for different routes like throttle:10,10 for a user login screen vs. throttle:60,1 for other routes because you hear you are suppose to rate limit resources according to their typical usage. Instead of working you find your users hit rate limits because they made a lot of requested against one route that tripped the rate limiter on another. So you raise the limits because that sounds like the simple fix but it turns out you just increased your attack surface area.

Problem 3: Not Extensible

If the user is logged in, Laravel does use the unique identifier for the user as the key which is better than the IP address. Even better, different rates for different users using a string based key like throttle:60|rate_limit which translates to 60 requests by guests and whatever Auth::user()->rate_limit returns for users (or if you use the Laravel suggested throttle:rate_limit value, it will actually be the same as throttle:0 for guests). That's all fine but you want to rate limit the resources the user accesses differently. The only answer to that problem is to hack the Illuminate\Routing\Middleware\ThrottleRequests middleware and overload the resolveRequestSignature() method to return your custom key. Oh, and let's not forget that the same decay rate is used for both guests and authenticated users so you have to grok your way through that inadvertent security coupling.

Understanding the Leaky Bucket Algorithm

The answer to Laravel's rate limiter is a better algorithm that includes a couple of additional configuration settings.

Leaky Bucket Implementation

The Leaky Bucket Algorithm is the rate limiter this package implements. As its name suggests, there is a bucket (a cache) that you fill with drips (a request) up to the maximum capacity (the limit) at which point if you continue filling it will overflow (rate limiting). This bucket also leaks at a constant rate of drips per second (requests per second). This means that if you fill the bucket with drips at the same rate in which it leaks then you can continue hitting it forever without overflowing. You can also burst up to the maximum capacity which has no effect on the leak rate. So effectively the Leaky Bucket Algorithm enforces a constant drip rate determined not by the number of drips added to the bucket but by the leak rate in constant time. Since the algorithm tracks leaks and buckets and not just drips, buckets can be persisted for a longer time to track malicious activity longer and rate limit a more balanced request load.

Solution 1: Bursting Limit

As already, explained bursting is an exploit that a hacker can use against the Laravel rate limiter and to monitor (increase the limits) the exploit makes the attack surface area even bigger. The bursting limit in a Leaky Bucket implementation is a separate limit that does not expire in a binary, all or nothing, way but expires one drip at a time as the bucket leaks. With this implementation you set the bursting limit and that limit drains slowly over time at the constant leak rate. This means that so long as the client does not exceed the limits and enters a timeout, the client can burst up to this limit then wait as little as the leak rate to make one more request. They can trickle in requests constantly so long as they don't overflow the bucket's maximum capacity.

For example if the settings were throttle:60,1 then the user can burst up in the first second to 60 requests, and only

View on GitHub
GitHub Stars148
CategoryDevelopment
Updated1mo ago
Forks15

Languages

PHP

Security Score

100/100

Audited on Feb 23, 2026

No findings