Custom Throttle Middleware

James Bannister • July 24, 2019 • 4 minute read

laravel

I've said it before, and I'll say it again, one of the biggest benefits of using a framework is that so many common use-cases or problems have already been solved - and throttling requests is no different.

Out of the box, Laravel ships with a ThrottleRequests middleware that is easily configurable to provide the number of requests an IP address can make over a period of time.

For example, the below snippet in a routes file would restrict the number of requests a user or IP address could make to 60 per minute:

1Route::middleware('throttle:60,1')->group(function () {
2 Route::get('/products', function () {
3 //
4 });
5 Route::get('/locations', function () {
6 //
7 });
8});

Since Laravel 5.6, we've even be able to do dynamic rate limiting, based on an attribute of the User model; representing a column in your users table - the below example demonstrates a column called rate_limit dictating the number of requests a user can make per hour:

1Route::middleware('throttle:rate_limit,60')->group(function () {
2 Route::get('/products', function () {
3 //
4 });
5 Route::get('/locations', function () {
6 //
7 });
8});

You could set this to 3,600 by default, and then bump this up or down for particular users as their needs dictate.

However, what if you want to throttle a particular route (or routes) but not user either of these approaches?

In a recent project, users were able to add IoT devices and get a unique URL they could use as a webhook to send data to our system. Using a unique key in the URL, I needed to be able to throttle the requests they could make.

This is actually quite easy to implement once you know how - we just need to understand a little about how the existing ThrottleRequests middleware works.

If you open the ThrottleRequests middleware and check out the handle() method, we can see the following:

1public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
2{
3 $key = $this->resolveRequestSignature($request);
4 
5 $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);
6 
7 if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
8 throw $this->buildException($key, $maxAttempts);
9 }
10 
11 $this->limiter->hit($key, $decayMinutes * 60);
12 
13 $response = $next($request);
14 
15 return $this->addHeaders(
16 $response, $maxAttempts,
17 $this->calculateRemainingAttempts($key, $maxAttempts)
18 );
19}

The first line of the method is what we're interested in:

1$key = $this->resolveRequestSignature($request);

This is where the ThrottleRequests middleware figures out the key that it should use to track your requests.

The method on the the ThrottleRequests middleware looks like this:

1protected function resolveRequestSignature($request)
2{
3 if ($user = $request->user()) {
4 return sha1($user->getAuthIdentifier());
5 }
6 
7 if ($route = $request->route()) {
8 return sha1($route->getDomain().'|'.$request->ip());
9 }
10 
11 throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
12}

If you are an authenticated user, then it will use the key name (usually id unless you have overridden this) of your User model. Otherwise, it will use the domain and IP address of the request as the key.

Knowing this, we just need to override the resolveRequestSignature() method to derive our own key for the request:

  1. Add a new file in app/Http/Middleware called CustomThrottleMiddleware (or whatever you want to call this middleware) or scaffold some new middleware with php artisan make:middleware CustomThrottleMiddleware
  2. Replace the contents with:
1<?php
2 
3namespace App\Http\Middleware;
4 
5use Illuminate\Routing\Middleware\ThrottleRequests;
6 
7class CustomThrottleMiddleware extends ThrottleRequests
8{
9 protected function resolveRequestSignature($request)
10 {
11 //
12 }
13}

What we have done is create our own new middleware, that extends the existing ThrottleRequests middleware, and is now setup to override the resolveRequestSignature() method so that we can set our own key.

  1. Now you just need to add your code to the resolveRequestSignature() method to actually return a key - I'll include some examples below:
1protected function resolveRequestSignature($request)
2{
3 // Throttle by a particular header
4 return $request->header('API-Key');
5}
1protected function resolveRequestSignature($request)
2{
3 // Throttle by a request parameter
4 return $request->input('account_id');
5}
1protected function resolveRequestSignature($request)
2{
3 // Throttle by session ID
4 return $request->session();
5}
1protected function resolveRequestSignature($request)
2{
3 // Throttle by IP to a particular Model in the route - where `product` is our model and this route is set to use `product` with route model binding
4 return $request->route('product') . '|' . $request->ip();
5}

In my use case, I opted for throttling by a number of parameters. The unique URL we provided a user was unique to them, but was the same for all IoT devices of the same type. For example, IoT devices we supported came in type A, B, and C; if you had two of type A then they shared the same unique URL. Our reasoning for this isn't important to this post, so I'll leave that out.

Each device was allowed to make one request every 5 minutes, however as two devices of type A were technically two separate devices, we needed to include an additional way of separating the two.

To achieve this, we just figured out what type of IoT device we were working with and then resolved the token as appropriate from there:

1protected function resolveRequestSignature($request)
2{
3 $token = $request->route()->parameter('deviceByToken');
4 
5 $device = IotDevice::findByToken($token)->firstOrFail();
6 
7 if ($device->type === 'A')
8 {
9 return $token . '-' . $request->input('unique_parameter_for_type_a_per_device');
10 } else if ($device->type === 'B')
11 {
12 return $token . '-' . $request->input('unique_parameter_for_type_b_per_device');
13 } if ($device->type === 'C')
14 {
15 return $token . '-' . $request->input('unique_parameter_for_type_c_per_device');
16 }
17 
18 return $token;
19}
  1. Last but not least, you then just need to register the middleware for use by updating your app/Http/Kernel.php file like so:
1protected $routeMiddleware = [
2 //...
3 'customthrottle' => \App\Http\Middleware\CustomThrottleMiddleware::class,
4];

This then allows you to use your new middleware like so (replacing the 1 and 5 to suit your needs):

1Route::middleware('customthrottle:1,5')->group(function () {
2 Route::get('/products', function () {
3 //
4 });
5 Route::get('/locations', function () {
6 //
7 });
8});

Using the above approach, we were able to throttle requests using a unique route parameter, present in each of the requests, along with a unique identifier that each device was including in their payload for the request.


I hope you found this useful. If you have any feedback, thoughts, or different approaches to this then I'd love to hear from you; I'm @jryd_13 on Twitter.

Thanks!