Rate Limiting Notifications in Laravel

Scott's Photo by Scott Wakefield

Please note: this article was written for Laravel 5.3. Some method signatures may have changed since then.

Laravel Notifcations allow you to easily send users notifications across different channels, including email and SMS. It’s a powerful and incredibly useful feature, but depending on your application you may need to limit the amount of notifications your users receive.

We recently had this exact request from a client. Their application notifies users, across multiple teams, when a product is updated – the notifications themselves are fairly generic and are intended to just give the user a little ‘nudge’ to visit the product page to see the updates.

Under the hood, the application uses events and event listeners to determine when it needs to send a notification.

Whenever a user updates a product, we fire an ProductUpdated event. A separate SendProductNotifications listener is responsible for sending out the ProductUpdated notifications when it hears the ProductUpdated event.

The event carries data about the product that was updated; using this data the listener can work out which users need notifying. In this case, this is essentially all the administrators of Hubs that have access to a Product.

The issue we had was that users could make (and were making) multiple, minor updates to products in a short space of time, which caused administrators to get notified multiple times about updates to the same project. This was compounded by the fact that a user could be an administrator of multiple Hubs.

It was at this point we started to look at how we could rate limit the amount of 'product updated' notifications a user received within a specified timeframe.

A bit like Login Throttling, right?!

The first thing that came to mind when mapping out some solutions to our problem was Laravel’s Login Throttling feature. Here’s how it is described in the Laravel documentation:

“If you are using Laravel's built-in LoginController class, the Illuminate\Foundation\Auth\ThrottlesLogins trait will already be included in your controller. By default, the user will not be able to login for one minute if they fail to provide the correct credentials after several attempts. The throttling is unique to the user's username / e-mail address and their IP address.”

This sounded like what we needed, but instead of throttling login attempts based on a user’s username, e-mail and IP address, we wanted to throttle Notifications based on the notification type, a user's ID, and the updated product's ID.

Leveraging Laravel’s RateLimiter class

As mentioned in the documentation, Laravel provides its login throttling logic through the Illuminate\Foundation\Auth\ThrottlesLogins trait. From the trait we could determine what we would need to implement to achieve out goal:

We would need to use Illuminate\Cache\RateLimiter to keep track of the amount of notification attempts. Specifically its hit() and tooManyAttempts() methods, both of which require:

  1. a string key you want to use to identify an attempt
  2. the decay minutes – the number of minutes to throttle for

tooManyAttempts() also requires us to pass through the maximum amount of attempts that should be allowed, which in our case will always be 1.

Now that we knew how we’d manage the rate limiting, we needed to work out what to change in order to insert that logic into Laravel’s native 'send notification' behaviour.

Customising Notifiable

As per the official Notifications documentation we were already using the Notifiable trait on our User model. The trait is composed of two additional traits:

  1. HasDatabaseNotifications - adds the methods needed to pull notifications from the database
  2. RoutesNotifications - adds the notify() method thats used to send a given notification

It was clear that we would need to swap RoutesNotifications out with our own implementation that customised the notify() method.

Routing Throttled Notifications

Using our own notify method was pretty simple. We created two new traits:

  1. App\Notifications\RoutesThrottledNotifications – where our customised notify() method would live
  2. App\Notifications\Notifiable – a copy of the Illuminate\Notifications\Notifiable trait, that used our new RoutesThrottledNotifications trait.

App\Notifications\RoutesThrottledNotifications looks like this:

You’ll notice that we determine which notifications should be rate limited using the ThrottledNotification interface. This also ensures that the Notification has both its own throttleDecayMinutes() and throttleKeyId() defined.

Overall, this approach has allowed us to leverage code within the Laravel codebase and add a useful feature without having to modify much of the existing site code at all.

Ready to get started on that new web project?
Let's get to work! →