PHPackages                             bouzentm/laravel-queue-debounce - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. bouzentm/laravel-queue-debounce

ActiveLibrary[Caching](/categories/caching)

bouzentm/laravel-queue-debounce
===============================

Dispatch-time debounce for Laravel queued jobs. One job per debounce window, atomic Redis gating, crash recovery.

v1.0.3(1mo ago)03MITPHPPHP ^8.3

Since Jul 2Pushed 1mo agoCompare

[ Source](https://github.com/m2cci-bouzentm/laravel-queue-debounce)[ Packagist](https://packagist.org/packages/bouzentm/laravel-queue-debounce)[ RSS](/packages/bouzentm-laravel-queue-debounce/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (6)Versions (5)Used By (0)

Laravel Queue Debounce
======================

[](#laravel-queue-debounce)

Dispatch-time debounce for Laravel queued jobs. One job per debounce window, atomic Redis gating, crash recovery.

Works with **any Laravel queue driver**: Redis, SQS, Database, etc.

**The problem:** Webhooks, event listeners, and real-time triggers fire multiple times for the same entity within seconds. Without debouncing, you get duplicate jobs flooding your queue.

**This package** gates at dispatch time using Redis GETSET — only 1 job enters the queue per debounce window. Subsequent calls are no-ops (nothing queued).

How it works
------------

[](#how-it-works)

```
Time: 0s     5s      10s     30s     35s
      |      |       |       |       |
      v      v       v       v       v
    call   call    call   [executes] call
      |______|_______|          |_____|
           |                        |
    These 3 calls become       This call
    ONE execution              starts new window

```

1. First `::debounce()` → sets Redis key, queues job with delay
2. Subsequent calls within the window → Redis key exists, skip (nothing queued)
3. Job executes → middleware cleans up Redis key
4. Next call → starts a new debounce window

### vs ShouldBeUnique / ShouldBeUniqueUntilProcessing

[](#vs-shouldbeunique--shouldbeuniqueuntilprocessing)

FeatureShouldBeUniqueThis packageLock held duringProcessing onlyConfigurable delay windowLock releasedAfter handle() finishesAfter handle(), only if not releasedCrash recoveryLock expires via TTLDetects expired timestamps, re-queuesQueue stats1 job (clean)1 job (clean)`release()` / `$tries` / `failed()`WorksWorksCooldown after executionNoYes (delay window)Installation
------------

[](#installation)

```
composer require bouzentm/laravel-queue-debounce
```

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12+
- Redis (phpredis extension)

Usage
-----

[](#usage)

### Basic usage

[](#basic-usage)

```
use Bouzentm\LaravelQueueDebounce\Debounceable;

class SyncContactJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use Debounceable;

    protected function debounceKey(): string
    {
        return 'sync-contact:' . $this->contactId;
    }

    public function __construct(public int $contactId)
    {
        $this->debounceDelay = 30; // seconds
    }

    public function handle(): void
    {
        Contact::find($this->contactId)->syncToCrm();
    }
}

// In your listener or model:
class Contact extends Model
{
    protected static function booted(): void
    {
        static::saved(function (Contact $contact) {
            // Even if called 100 times in 30 seconds, only ONE job executes
            SyncContactJob::debounce($contact->id);
        });
    }
}
```

### Multiple arguments

[](#multiple-arguments)

The debounce key is defined by you, so you control granularity:

```
class UpdateTicketJob implements ShouldQueue
{
    use Debounceable;

    protected function debounceKey(): string
    {
        return "update-ticket:{$this->ticketId}:{$this->updateType}";
    }

    public function __construct(
        public int $ticketId,
        public string $updateType
    ) {
        $this->debounceDelay = 60;
    }
}

// These are DIFFERENT debounce windows:
UpdateTicketJob::debounce(123, 'status');   // Window 1
UpdateTicketJob::debounce(123, 'priority'); // Window 2
UpdateTicketJob::debounce(456, 'status');   // Window 3
```

### Merging with other middleware

[](#merging-with-other-middleware)

The trait defines `middleware()` which cleans up the Redis key after `handle()` runs. If your job needs additional middleware (e.g. `WithoutOverlapping`), override `middleware()` and merge with `debounceMiddleware()`:

```
public function middleware(): array
{
    return [
        ...$this->debounceMiddleware(),
        new WithoutOverlapping($this->contactId),
    ];
}
```

Configuration
-------------

[](#configuration)

PropertyDefaultDescription`$debounceDelay``60`Seconds to delay execution. During this window, subsequent `debounce()` calls are no-ops.`debounceKey()`(abstract)Unique key for the debounce window. Include entity identifiers for proper scoping.### Custom debounce delay (PHP 8.4+)

[](#custom-debounce-delay-php-84)

The trait declares `protected int $debounceDelay = 60`. On PHP 8.4+, redeclaring the property in your job with a different default causes a `FatalError`:

```
App\Jobs\MyJob and Debounceable define the same property ($debounceDelay)
in the composition of App\Jobs\MyJob. However, the definition differs
and is considered incompatible.

```

Set the delay in your constructor instead:

```
class PrepareReplyJob implements ShouldQueue
{
    use Debounceable;

    // Don't redeclare $debounceDelay here

    public function __construct(public int $contactId)
    {
        $this->debounceDelay = 1200; // 20 minutes
    }
}
```

Crash recovery
--------------

[](#crash-recovery)

If a job crashes without cleanup (worker killed, OOM, etc.), the Redis key holds an expired timestamp. The next `debounce()` call detects this and re-queues:

```
T=0s   Job queued, Redis key set to T+30
T=30s  Worker crashes — Redis key still holds T+30
T=45s  New event → GETSET returns T+30 → T+30 release()` (e.g. to retry later), the Redis key is **not** deleted — the debounce window stays active. The key is only cleaned up after a successful execution that doesn't release back to queue.

How it works internally
-----------------------

[](#how-it-works-internally)

Uses Redis `GETSET` for atomic dispatch-time gating:

1. `GETSET key new_timestamp` — atomically reads old value, writes new
2. If old value is `false` (no job pending) or expired (crashed) → queue the job
3. If old value is in the future → job already pending, skip
4. Middleware runs after `handle()` → deletes key only if not released → opens window for next cycle

The first event triggers execution after the delay. Subsequent events during the window are dropped.

Testing
-------

[](#testing)

```
use Illuminate\Support\Facades\Redis;

it('debounces multiple calls into one job', function () {
    Redis::shouldReceive('getset')
        ->once()->andReturn(false)   // first call: no key
        ->once()->andReturn(now()->addSeconds(30)->getTimestamp()); // second: pending

    Redis::shouldReceive('expire')->twice();

    SyncContactJob::debounce(123);
    SyncContactJob::debounce(123);

    Queue::assertPushed(SyncContactJob::class, 1);
});
```

License
-------

[](#license)

MIT

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance90

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

4

Last Release

48d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/94069180cbcfa2274446dcd8b46415c80155a2f82d8a92efaa4e96ba50d75ee0?d=identicon)[m2cci-bouzentm](/maintainers/m2cci-bouzentm)

---

Top Contributors

[![m2cci-bouzentm](https://avatars.githubusercontent.com/u/102865341?v=4)](https://github.com/m2cci-bouzentm "m2cci-bouzentm (9 commits)")

---

Tags

laravelredisqueuejobsdebouncededuplication

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/bouzentm-laravel-queue-debounce/health.svg)

```
[![Health](https://phpackages.com/badges/bouzentm-laravel-queue-debounce/health.svg)](https://phpackages.com/packages/bouzentm-laravel-queue-debounce)
```

###  Alternatives

[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.2k99.8M355](/packages/laravel-horizon)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k57.2M683](/packages/laravel-scout)[illuminate/broadcasting

The Illuminate Broadcasting package.

7127.4M237](/packages/illuminate-broadcasting)[nuwave/lighthouse

A framework for serving GraphQL from Laravel

3.5k12.2M128](/packages/nuwave-lighthouse)[yangusik/laravel-balanced-queue

Laravel queue management with load balancing between partitions (user groups)

8516.9k](/packages/yangusik-laravel-balanced-queue)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M154](/packages/laravel-pulse)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
