Debounced Queued Listeners in Laravel 13.26 | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. [Laravel](https://www.msaied.com/public/articles?category=laravel)
6. /
7. Debounced Queued Event Listeners in Laravel 13.26

   [Laravel](https://www.msaied.com/public/articles?category=laravel) 

 Debounced Queued Event Listeners in Laravel 13.26
==================================================

 Laravel 13.26 extends the `#\[DebounceFor\]` attribute to queued event listeners, collapsing bursts of identical events into a single listener execution and eliminating redundant queue work.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 21 Aug 2026 · Updated 21 Aug 2026 · 4 min read

ShareCopy linkCopied

 ![Debounced Queued Event Listeners in Laravel 13.26](https://cdn.msaied.com/577/619bf19cd5810dc6898304bb01868b62.png) 

  On this page +1. [The Problem: Redundant Queue Work](#the-problem-redundant-queue-work)
2. [Debouncing a Queued Listener](#debouncing-a-queued-listener)
3. [How the Debounce Mechanism Works](#how-the-debounce-mechanism-works)
4. [Preventing Starvation with maxWait](#preventing-starvation-with-codemaxwaitcode)
5. [Rules and Constraints](#rules-and-constraints)
6. [Key Takeaways](#key-takeaways)

 The Problem: Redundant Queue Work
---------------------------------

A product import touches the same record forty times in a minute. `ProductUpdated` fires forty times. The listener that rebuilds the search index runs forty times, each run indexing state the next one immediately overwrites. The queue does exactly what it was told, but 97 percent of the work is waste.

What you actually want is for a burst of identical events to collapse into **one listener execution** at the end of the burst, carrying the latest state.

Laravel 13.6 introduced debounceable queued jobs. **Laravel 13.26** extends the same `#[DebounceFor]` attribute to queued event listeners, contributed by [@stevebauman](https://github.com/stevebauman) in [\#61169](https://github.com/laravel/framework/pull/61169), so event-driven code gets the same behavior without restructuring listeners into manually dispatched jobs.

Debouncing a Queued Listener
----------------------------

Add the `#[DebounceFor]` attribute to any listener that implements `ShouldQueue` and specify a debounce window in seconds:

```php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
    public function debounceId(ProductUpdated $event): string
    {
        return (string) $event->product->getKey();
    }

    public function handle(ProductUpdated $event): void
    {
        ProductIndexer::index($event->product->fresh());
    }
}

```

Every `ProductUpdated` event for product 42 within a 30-second window now results in **one `handle()` call**, made for the last event of the burst. Events for product 43 debounce independently because `debounceId()` keys the window per product.

Without a `debounceId`, all dispatches share one window — ideal for global listeners like "rebuild the sitemap". The ID can also be a plain `$debounceId` property when it does not depend on the event payload.

How the Debounce Mechanism Works
--------------------------------

Each dispatch queues the listener with a delay equal to the debounce window and records an **owner token** in the cache, keyed by listener class and debounce ID. A newer dispatch overwrites the token. When an older queued copy finally executes, it checks whether it still owns the token — if not, it discards itself silently.

One important caveat: a single event on an otherwise idle resource still waits out the full debounce window before executing. There is no "fire immediately on first event" shortcut.

Preventing Starvation with `maxWait`
------------------------------------

Pure debouncing has a failure mode: a continuous stream of events that never pauses long enough for the window to expire defers the listener indefinitely.

`maxWait` solves this. With `#[DebounceFor(30, maxWait: 120)]`, once dispatches have been pushing the window for 120 seconds, the next dispatch executes **without delay** instead of extending the deferral again. A busy import still gets its writes collapsed — roughly one index run per two minutes — rather than either forty runs or zero.

Rules and Constraints
---------------------

Three things to know before rolling this out:

- **No `ShouldBeUnique` together.** Combining the two attributes throws a `LogicException` at dispatch time. They hold opposite semantics — first-wins vs. last-wins — and the framework refuses to pick silently.
- **Debouncing is scoped to the listener, not the event.** Other listeners on `ProductUpdated` still run for every event. Only the attributed listener collapses.
- **Re-read state in the handler.** The event object that survives the debounce is the last one dispatched, but by execution time even it can be stale. The example above calls `$event->product->fresh()` for exactly this reason. Treat the event as a pointer to a resource, not as a complete payload.

That last habit is what makes debouncing safe: if the listener re-derives its output from the database, collapsing forty runs into one changes the cost, not the result.

Key Takeaways
-------------

- `#[DebounceFor(seconds, maxWait: seconds)]` on a `ShouldQueue` listener collapses event bursts into one execution.
- `debounceId()` scopes the debounce window per resource; omit it for global listeners.
- `maxWait` prevents indefinite deferral under sustained event streams.
- Cannot be combined with `ShouldBeUnique`.
- Always call `->fresh()` or re-query state inside the handler; the surviving event object may be stale.

---

*Source: [Debounced Queued Event Listeners in Laravel — Laravel News](https://laravel-news.com/laravel-debounced-queued-listeners)*

- [Laravel](https://www.msaied.com/public/articles?search=Laravel)
- [Queues](https://www.msaied.com/public/articles?search=Queues)
- [Events](https://www.msaied.com/public/articles?search=Events)
- [Laravel 13](https://www.msaied.com/public/articles?search=Laravel%2013)
- [Performance](https://www.msaied.com/public/articles?search=Performance)

 Frequently asked questions 
---------------------------

  What does the `#\[DebounceFor\]` attribute do on a Laravel queued event listener?It collapses a burst of identical events into a single listener execution. Each dispatch queues the listener with a delay equal to the debounce window and records an owner token in the cache. If a newer dispatch arrives before the delay expires, it overwrites the token and the older queued copy discards itself when it runs, leaving only the last dispatch to execute.

   How does `maxWait` prevent a debounced listener from never running under a continuous event stream?Without `maxWait`, a stream of events that never pauses for the full debounce window would defer the listener indefinitely. Setting `maxWait` caps the total deferral time: once dispatches have been pushing the window for that many seconds, the next dispatch executes immediately instead of extending the delay again.

   Can `#\[DebounceFor\]` be combined with `ShouldBeUnique` on the same listener?No. Combining them throws a `LogicException` at dispatch time. `ShouldBeUnique` is first-wins (only the first job in the window runs) while `#\[DebounceFor\]` is last-wins (only the most recent dispatch runs). The framework treats the combination as a logic error rather than silently picking one behavior.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleAgent Run Observability in Laravel AI SDK 0.11](https://www.msaied.com/public/articles/agent-run-observability-in-laravel-ai-sdk-011) [Next articlePractical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://www.msaied.com/public/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-3)  

   On this page
-------------

1. [The Problem: Redundant Queue Work](#the-problem-redundant-queue-work)
2. [Debouncing a Queued Listener](#debouncing-a-queued-listener)
3. [How the Debounce Mechanism Works](#how-the-debounce-mechanism-works)
4. [Preventing Starvation with maxWait](#preventing-starvation-with-codemaxwaitcode)
5. [Rules and Constraints](#rules-and-constraints)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
