Rate Limiting Notifications in Laravel using Laravel's native rate limiter to avoid flooding users with duplicate notifications.
Laravel | PHP | Laravel-Notification-Rate-Limit | Date |
---|---|---|---|
7.x/8.x | 7.1/8.0 | 1.1.0 | 2021-05-20 |
9.x | 8.0 | 2.1.0 | 2023-08-26 |
10.x | 8.0/8.1 | 2.1.0 | 2023-08-26 |
10.x | 8.2/8.3 | 2.2.0 | 2024-03-18 |
10.x | 8.2/8.3 | 3.0.0 | 2024-05-25 |
11.x | 8.2/8.3 | 2.2.0 | 2024-03-18 |
11.x | 8.2/8.3 | 3.0.0 | 2024-05-25 |
You can install the package via composer:
composer require jamesmills/laravel-notification-rate-limit
Implement the ShouldRateLimit
interface and add the RateLimitedNotification
trait to the Notifications you would like to rate limit.
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Jamesmills\LaravelNotificationRateLimit\RateLimitedNotification;
use Jamesmills\LaravelNotificationRateLimit\ShouldRateLimit;
class NotifyUserOfOrderUpdateNotification extends Notification implements ShouldRateLimit
{
use Queueable;
use RateLimitedNotification;
...
Everything in this package has opinionated global defaults. However, you can override everything in the config, and many options may also be further customized on a per-notification basis (see below).
Publish it using the command below.
php artisan vendor:publish --provider="Jamesmills\LaravelNotificationRateLimit\LaravelNotificationRateLimitServiceProvider"
If you are upgrading from version 2, be aware that the NotificationRateLimitReached
event signature has changed, and now includes more information about the notification being skipped. If you have implemented your own version of this event class, you will need to update the constructor signature to accept these additional parameters. No other changes should be required as a part of the upgrade process.
Rate limiting is checked only when notifications are actually being delivered.
If a notification is sent to a queue, or a notification is dispatched with a delay (e.g. $user->notify($notification->delay(...))
), then any rate limiting will be considered only when the notification is actually about to be dispatched to the user.
If you have multiple models that use the Notifiable trait (e.g. multiple types of User models), you should add the class name of the Notifiable instance to the cache key (see Customizing the Notifiasble identifier below).
By default, the NotificationRateLimitReached
event will be fired when a Notification is skipped. You can customise this using the event
option in the config.
By default, a rate-limited Notification will be rate-limited for 60
seconds.
Update globally with the rate_limit_seconds
config setting.
Update for an individual basis by adding the below to the Notification:
// Change rate limit to 1 hour
protected $rateLimitForSeconds = 3600;
By default, this package will log all skipped notifications.
Update globally with the log_skipped_notifications
config setting.
Update for an individual basis by adding the below to the Notification:
// Do not log skipped notifications
protected $logSkippedNotifications = false;
By default, the Rate Limiter uses a cache key made up of some opinionated defaults. One of these default keys is serialize($notification)
. You may wish to turn this off.
Update globally with the should_rate_limit_unique_notifications
config setting.
Update for an individual basis by adding the below to the Notification:
protected $shouldRateLimitUniqueNotifications = false;
You may want to customise the parts used in the cache key. You can do this by adding code such as the below to your Notification:
public function rateLimitCustomCacheKeyParts()
{
return [
$this->account_id
];
}
By default, we use the primary key or $id
field on the Notifiable
instance to identify the recipient of a notification.
If for some reason you do not want to use $id
, you can add a rateLimitNotifiableKey()
method to your Notifiable
model and return a string containing the key to use.
For example, if multiple users could belong to a group and you only want one person (any person) in the group to receive the notification, you might return the group ID instead of the user ID:
class User extends Authenticatable
{
use Notifiable;
protected $fillable = ['id', 'name', 'email', 'groupId'];
public function rateLimitNotifiableKey(): string
{
return $this->group_id;
}
}
Similarly, if you have multiple models in your application that are Notifiable
, using only the id
could result in collisions (where, for example, Agent
#41 receives a notification that then precludes Customer
#41 from receiving a similar notification). In this case, you may want to return an identifier that also includes the class name in the key for each model:
class Customer extends Authenticatable
{
use Notifiable;
protected $fillable = ['id', 'name', 'email'];
public function rateLimitNotifiableKey(): string
{
return get_class($this) . '#' . $this->id;
}
}
class Agent extends Authenticatable
{
use Notifiable;
protected $fillable = ['id', 'name', 'email'];
public function rateLimitNotifiableKey(): string
{
return get_class($this) . '#' . $this->id;
}
}
There may be circumstances where you wish to implement custom application logic for determining that a notification should be discarded even if the rate limiter itself would not prevent it from being sent (e.g. keeping track of, and setting an upper limit of, the number of times a given user can receive a specific notification in total).
To do so, add a rateLimitCheckDiscard
function to your notification, and return a non-NULL string to indicate the reason that a notification is being discarded. Example:
public function rateLimitCheckDiscard(string $key): ?string
{
$max_send_key = $key . '_send_count';
$count = Cache::get($max_send_key, 0);
if ($count >= 3) {
return 'Max send limit reached';
}
Cache::put($max_send_key, $count + 1);
return null;
}
Notes:
- The string 'Rate limit reached' (defined at
NotificationRateLimitReached::REASON_LIMITER
) is reserved to indicate that the rate limiter is preventing the notification from being dispatched. - If the rate limiter itself is preventing a notification from being dispatched, then the custom
rateLimitCheckDiscard
will not be called at all. - If
rateLimitCheckDiscard
returns a non-NULL string, then:- the notification will not be dispatched and it will be discarded; and
- the attempt will not be counted as a 'hit' against the rate limiter itself.
- The 'reason' returned from
rateLimitCheckDiscad
will be included in the log entry (if configured) and forwarded along to theNotificationRateLimitReached
event as well.
If you wish to defer/delay the delivery of a notification rather than completely discarding it (see issue #33), an example of one way that this could be implemented is available at https://github.com/tibbsa/lnrl_deferral_example/.
composer test
Please see CHANGELOG for more information what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security related issues, please email anthony@trinimex.ca and james@jamesmills.co.uk instead of using the issue tracker.
This package is 100% free and open-source, under the MIT License (MIT). Use it however you want.
This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.
Inspiration for this package was taken from the article Rate Limiting Notifications in Laravel by Scott Wakefield (now available only via the Internet Archive's Wayback Machine).