> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Queue Job Execution Control

> How to use ShouldBeUnique, ShouldBeUniqueUntilProcessing, and DebounceFor to control duplicate execution and burst dispatches of queued jobs.

## Overview

Laravel's queue system provides two strategies for controlling job execution: **Unique Jobs** and **Debounced Jobs**. Both prevent unnecessary work when the same job is dispatched multiple times, but they work differently.

| Feature                 | Interface / Attribute           | Purpose                                                                              |
| ----------------------- | ------------------------------- | ------------------------------------------------------------------------------------ |
| Unique Jobs             | `ShouldBeUnique`                | Ensures only one instance of a job exists on the queue                               |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | Holds the unique lock only until processing begins                                   |
| Debounced Jobs          | `#[DebounceFor]`                | When a job is dispatched many times in a short window, only the latest dispatch runs |

<Warning>
  Unique jobs and debounced jobs are **mutually exclusive**. A job using the `DebounceFor` attribute should not implement `ShouldBeUnique`.
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

Prevents duplicate dispatches while an instance of the job is already on the queue.

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // No additional methods required
}
```

While `UpdateSearchIndex` is on the queue (or being processed), any new dispatches of the same job will be silently ignored.

### Scoping Uniqueness with a Key — `UniqueFor` + `uniqueId()`

When the same job class handles different entities (e.g., product A vs. product B), define a `uniqueId()` method to scope the uniqueness constraint.

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;

#[UniqueFor(3600)] // Auto-release the lock after 1 hour
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

    public function uniqueId(): string
    {
        return (string) $this->productId;
    }
}
```

* The value returned by `uniqueId()` becomes the cache lock key.
* `#[UniqueFor(seconds)]` sets a timeout after which the lock is automatically released — a failsafe for stuck jobs.

### Customizing the Cache Driver — `uniqueVia()`

To use a specific cache driver instead of the default:

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function uniqueVia(): Repository
{
    return Cache::driver('redis');
}
```

<Info>
  Unique Jobs require a cache driver that supports atomic locks: `redis`, `database`, `memcached`, `dynamodb`, `file`, or `array`.
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

With `ShouldBeUnique`, the lock is held **until the job completes or exhausts its retry attempts**. This can be a problem in some scenarios.

**Example:** `UpdateSearchIndex(product_id: 42)` is on the queue. A worker picks it up and starts processing. You want to dispatch the same job again — but the lock is still held, so the new dispatch is ignored until processing finishes.

`ShouldBeUniqueUntilProcessing` releases the lock **immediately before processing begins**, allowing a new dispatch the moment a worker picks up the job.

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}
```

```mermaid theme={null}
sequenceDiagram
    participant D as Dispatcher
    participant Q as Queue
    participant W as Worker

    D->>Q: dispatch() — acquires lock
    D->>Q: dispatch() — lock held → ignored

    note over Q,W: ShouldBeUnique
    W->>Q: picks up job
    W->>W: processing (lock held)
    D->>Q: dispatch() — lock held → ignored
    W->>W: done — lock released
    D->>Q: dispatch() — now accepted

    note over Q,W: ShouldBeUniqueUntilProcessing
    W->>Q: picks up job — lock released
    D->>Q: dispatch() — no lock → accepted
    W->>W: processing
```

### Comparison

|                           | `ShouldBeUnique`           | `ShouldBeUniqueUntilProcessing` |
| ------------------------- | -------------------------- | ------------------------------- |
| Lock released             | After completion / failure | Before processing begins        |
| Dispatch while processing | Ignored                    | Accepted                        |
| Use case                  | Prevent all concurrency    | Allow re-queue while processing |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  The `DebounceFor` attribute was added in **Laravel 13**.
</Info>

When the same job is dispatched many times in a short window, only the **last dispatch** executes. This mirrors the debounce pattern from frontend development.

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)] // Only the last dispatch within 30 seconds runs
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $productId)
    {
    }

    public function debounceId(): string
    {
        return (string) $this->productId;
    }
}
```

* `debounceId()` identifies which dispatches are considered "the same job" (each `productId` is debounced independently).
* If `UpdateSearchIndex` for product 42 is dispatched 10 times within 30 seconds, only the last dispatch will run.

### `maxWait` — Capping the Deferral Window

For frequently updated data, debouncing could postpone execution indefinitely. Set `maxWait` to cap how long a job can be deferred.

```php theme={null}
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;
    // ...
}
```

In this example, the job will always run within 120 seconds of the first dispatch, even if new dispatches keep resetting the 30-second window.

### Customizing the Cache Driver — `debounceVia()`

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function debounceVia(): Repository
{
    return Cache::driver('redis');
}
```

### `JobDebounced` Event

When a job is superseded by a newer dispatch, Laravel fires `Illuminate\Queue\Events\JobDebounced` and removes the superseded job from the queue. Listen to this event for monitoring or logging.

***

## Choosing the Right Approach

```mermaid theme={null}
flowchart TD
    A["Same job dispatched multiple times"] --> B{"Burst dispatches —<br>only latest should run?"}
    B -->|Yes| C["#[DebounceFor]"]
    B -->|No| D{"Only one instance<br>on the queue at a time?"}
    D -->|Yes| E{"Prevent duplicates<br>even while processing?"}
    E -->|Yes| F["ShouldBeUnique"]
    E -->|No| G["ShouldBeUniqueUntilProcessing"]
    D -->|No| H["Standard job"]
```

| Use case                                       | Recommendation                  |
| ---------------------------------------------- | ------------------------------- |
| Running the same job twice is pointless        | `ShouldBeUnique`                |
| Prevent parallel execution entirely            | `ShouldBeUnique`                |
| Allow re-dispatch as soon as processing starts | `ShouldBeUniqueUntilProcessing` |
| User clicks save repeatedly — process once     | `#[DebounceFor]`                |
| Rebuild search index after bulk model updates  | `#[DebounceFor]` + `maxWait`    |

***

## Internal Implementation

### Lock Key Format for Unique Jobs

When a `ShouldBeUnique` job is dispatched, Laravel acquires a cache [atomic lock](/cache#atomic-locks) using the following key format:

```
laravel_unique_job:{JobClassName}:{uniqueId()}
```

If the lock is already held, the job is not added to the queue.

### How Debounced Jobs Work

`DebounceFor` uses a cache entry to manage a "debounce window". On each new dispatch:

1. The existing queued job is removed (`JobDebounced` event fired)
2. The new job is added to the queue with a delay equal to the debounce seconds
3. The debounce timer resets

When `maxWait` is specified, the timestamp of the first dispatch is also recorded, capping the total deferral time.

***

## Reference

* [Laravel Docs — Unique Jobs](https://laravel.com/docs/queues#unique-jobs)
* [Laravel Docs — Debounced Jobs](https://laravel.com/docs/queues#debounced-jobs)
* [`Illuminate\Contracts\Queue\ShouldBeUnique`](https://github.com/laravel/framework/blob/main/src/Illuminate/Contracts/Queue/ShouldBeUnique.php)
* [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/main/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php)
* [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/main/src/Illuminate/Queue/Attributes/DebounceFor.php)
* [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/main/src/Illuminate/Queue/Attributes/UniqueFor.php)


## Related topics

- [Queues](/en/queues.md)
- [Laravel Telescope hands-on techniques](/en/blog/telescope-introduction.md)
- [Laravel Telescope](/en/telescope.md)
- [May 2026 Laravel updates](/en/blog/changelog/202605.md)
- [Context](/en/context.md)
