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 |
Unique jobs and debounced jobs are mutually exclusive. A job using the DebounceFor attribute should not implement ShouldBeUnique.
Unique Jobs — ShouldBeUnique
Prevents duplicate dispatches while an instance of the job is already on the queue.
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.
- 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:
Unique Jobs require a cache driver that supports atomic locks: redis, database, memcached, dynamodb, file, or array.
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.
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]
The DebounceFor attribute was added in Laravel 13.
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.
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.
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()
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
| 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
When a ShouldBeUnique job is dispatched, Laravel acquires a cache atomic lock using the following key format:
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:
- The existing queued job is removed (
JobDebounced event fired)
- The new job is added to the queue with a delay equal to the debounce seconds
- The debounce timer resets
When maxWait is specified, the timestamp of the first dispatch is also recorded, capping the total deferral time.
Reference