Skip to main content

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.
FeatureInterface / AttributePurpose
Unique JobsShouldBeUniqueEnsures only one instance of a job exists on the queue
Unique Until ProcessingShouldBeUniqueUntilProcessingHolds 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

ShouldBeUniqueShouldBeUniqueUntilProcessing
Lock releasedAfter completion / failureBefore processing begins
Dispatch while processingIgnoredAccepted
Use casePrevent all concurrencyAllow 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 caseRecommendation
Running the same job twice is pointlessShouldBeUnique
Prevent parallel execution entirelyShouldBeUnique
Allow re-dispatch as soon as processing startsShouldBeUniqueUntilProcessing
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 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:
  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

Last modified on July 14, 2026