zackaj/laravel-debounce
Debounce Laravel jobs, notifications, and (Laravel 11+) artisan commands to prevent spam and reduce queue load. Uses unique locks and caching to delay execution until activity stops. Tracks each request occurrence with reports including IP and authenticated user.
by zackaj
Laravel-debounce allows you to accumulate / debounce a job, notification or command to avoid spamming your users and your app's queue.
It also tracks and registers every request occurrence and gives you a nice report tracking with information like ip address and authenticated user per request.
This laravel package uses UniqueJobs (atomic locks) and caching to run only one instance of a task in a debounced interval of x seconds delay.
Everytime a new activity is recorded (occurrence), the execution is delayed by x seconds.
[!WARNING] Debouncing artisan commands requires laravel version >=11
A debounced notification to bulk notify users about new uploaded files.
https://github.com/user-attachments/assets/b1d5aafd-256d-4f6f-b31a-0d6dc516793b
FileUploaded.php
<?php
namespace App\Notifications;
use App\Models\File;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class FileUploaded extends Notification
{
use Queueable;
public function __construct(public File $file) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'files' => $this->file->user->files()
->where('created_at', '>=', $this->file->created_at)
->get(),
];
}
}
DemoController.php
<?php
namespace App\Http\Controllers;
use App\Models\File;
use App\Models\User;
use App\Notifications\FileUploaded;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Zackaj\LaravelDebounce\Facades\Debounce;
class DemoController extends Controller
{
public function normalNotification(Request $request)
{
$user = $request->user();
$file = File::factory()->create(['user_id' => $user->id]);
$otherUsers = User::query()->whereNot('id', $user->id)->get();
Notification::send($otherUsers, new FileUploaded($file));
return back();
}
public function debounceNotification(Request $request)
{
$user = $request->user();
$file = File::factory()->create(['user_id' => $user->id]);
$otherUsers = User::query()->whereNot('id', $user->id)->get();
Debounce::notification(
notifiables: $otherUsers,
notification:new FileUploaded($file),
delay: 5,
uniqueKey:$user->id,
);
return back();
}
}
composer require zackaj/laravel-debounce
Optionally publish the config file:
php artisan vendor:publish --tag=laravel-debounce-config
This will publish config/debounce.php:
return [
/*
* When set to false, debouncing is bypassed entirely and jobs, notifications
* and commands are fired immediately as if debounce was never called.
*/
'enabled' => env('LARAVEL_DEBOUNCE_ENABLED', true),
'driver' => 'cache', // the only supported driver for now
];
You can also toggle debouncing via your .env file:
LARAVEL_DEBOUNCE_ENABLED=false
This is useful for local development or testing environments where you want to disable debouncing without changing any code.
You can debounce existing jobs, notifications and commands with zero setup.
[!NOTE] you can't access report tracking without extending the package's classes, see Advanced usage.
use Zackaj\LaravelDebounce\Facades\Debounce;
//job
Debounce::job(
job:new Job(),//replace
delay:5,//delay in seconds
uniqueKey:auth()->user()->id,//debounce per Job class name + uniqueKey
sync:false, //optional, job will be fired to the queue
);
//notification
Debounce::notification(
notifiables: auth()->user(),
notification: new Notification(),//replace
delay: 5,
uniqueKey: auth()->user()->id,
sendNow: false,
);
//command
Debounce::command(
command: 'app:command',//replace
delay: 5,
uniqueKey: $request->ip(),
parameters: ['name' => 'zackaj'],//see Artisan::call() signature
toQueue: false,//optional, send command to the queue when executed
outputBuffer: null,//optional, //see Artisan::call() signature
);
In order to use:
your existing jobs, notifications and commands must extend:
use Zackaj\LaravelDebounce\DebounceJob;
use Zackaj\LaravelDebounce\DebounceNotification;
use Zackaj\LaravelDebounce\DebounceCommand;
or just generate new ones using the available make commands.
php artisan make:debounce-notification TestNotification
php artisan make:debounce-job TestJob
php artisan make:debounce-command TestCommand
Alternatively, now you can debounce from the job, notification and command instances directly without using the Debounce facade used in Basic usage
(new Job())->debounce(...);
(new Notification())->debounce(...);
(new Command())->debounce(...);
Laravel-debounce uses the cache to store every request occurrence, use getReport() method within your debounceables to access the report chain that has a collection of occurrences.
Every report will have one occurrence minimum.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Zackaj\LaravelDebounce\DebounceJob;
class Jobless extends DebounceJob implements ShouldQueue
{
use Dispatchable;
public function handle(): void
{
$this->getReport()->occurrences;//collection of occurrences
$this->getReport()->occurrences->count();
$this->getReport()->occurrences->first()->happenedAt;
$this->getReport()->occurrences->first()->ip;
$this->getReport()->occurrences->first()->ips;
$this->getReport()->occurrences->first()->requestHeaders;//HeaderBag
$this->getReport()->occurrences->first()->user;//authenticated user | null
}
}
If you wish to run some code before and/or after firing the debounceables you can use the available hooks.
Important: after() hook could run before your debounceable is handled if it's sent to the queue when:
sendNow==false and your notification implements ShouldQueuesync==false and your job implements ShouldQueuetoQueue==true (command)see: Basic usage
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
public function before(): void
{
//run before dispatching the job
}
public function after(): void
{
//run after dispatching the job
}
}
You get the $notifiables injected into the hooks.
<?php
...
class FileUploaded extends DebounceNotification
{
...
public function before($notifiables): void
{
//run before sending the notification
}
public function after($notifiables): void
{
//run after sending the notification
}
}
Due to limitations, the hook methods must be static.
<?php
...
class Test extends DebounceCommand
{
...
public static function before(): void
{
//run before executing the command
}
public static function after(): void
{
//run after executing the command
}
}
By default laravel-debounce debounces from the last occurrence happenedAt timestamp
public function getLastActivityTimestamp(): ?Carbon
{
return $this->getReport()->occurrences->last()->happenedAt;
}
You can override this method in your debounceables in order to debounce from a custom timestamp of your choice. If null is returned the debouncer will fallback to the default implementation above.
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
public function getLastActivityTimestamp(): ?Carbon
{
return Message::latest()->first()?->seen_at;
}
}
You get the $notifiables injected into the method.
<?php
...
class FileUploaded extends DebounceNotification
{
...
public function getLastActivityTimestamp(mixed $notifiables): ?Carbon
{
return $this->file->user->files->latest()->first()?->created_at;
}
}
Due to limitations, the method must be static.
<?php
...
class Test extends DebounceCommand
{
...
public static function getLastActivityTimestamp(): ?Carbon
{
return User::latest()->first()?->created_at;
}
}
For fun, you can actually debounce commands from the CLI using the debounce:command Artisan command.
php artisan debounce:command 5 uniqueKey app:test
here's the signature for the command:
php artisan debounce:command {delay} {uniqueKey} {signature*}
I recommend using Laravel telescope to see the debouncer live in the queues tab and to debug any failures.
When running tests, you may want to disable debouncing so jobs, notifications and commands are fired immediately without any debounce logic.
Add this to your phpunit.xml or .env.testing:
LARAVEL_DEBOUNCE_ENABLED=false
Or disable it per test:
public function test_something()
{
config(['debounce.enabled' => false]);
// debouncing is bypassed, everything fires immediately
}
1- If you clear / flush the cache, the report tracking and the registered dispatches will be lost.
2- Debouncing artisan commands requires laravel version >=11
Contributions, issues and suggestions are always welcome! See contributing.md for ways to get started.
How can I help you explore Laravel packages today?