All posts

May 8, 2026

PHP Attributes for Laravel Queue Jobs

WithoutRelations, UniqueFor, DebounceFor: the same attribute-driven approach from Eloquent models has arrived in queue jobs.


Laravel 13 extends the PHP attribute pattern from Eloquent models into queue jobs. The same problems exist: serialization behaviour, uniqueness constraints, and retry logic traditionally lived in methods or constructor gymnastics. Attributes move that configuration onto the class where it belongs.


#[WithoutRelations]

When a job is dispatched, Laravel serializes the job's constructor arguments. If one of those arguments is an Eloquent model, its loaded relationships get serialized too. That means the job payload can balloon unexpectedly, and by the time the worker picks it up the relationship data may be stale.

The old fix was explicit:

public function __construct(Podcast $podcast)
{
    $this->podcast = $podcast->withoutRelations();
}

With #[WithoutRelations], you annotate the parameter directly:

use Illuminate\Queue\Attributes\WithoutRelations;

public function __construct(
    #[WithoutRelations]
    public Podcast $podcast,
) {}

Laravel strips the relationships before serialization. The model itself is still serialized using the SerializesModels trait; only the eagerly loaded relations are dropped.

If every model in the job needs the same treatment, apply the attribute at the class level:

#[WithoutRelations]
class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public Podcast $podcast,
        public DistributionPlatform $platform,
    ) {}
}

All model arguments are stripped of relations when the job is queued.


#[UniqueFor]

ShouldBeUnique prevents duplicate jobs from sitting in the queue at the same time. The uniqueness lock expires after a timeout, configurable via a uniqueFor() method:

// Before
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public int $uniqueFor = 3600;

    public function uniqueId(): string
    {
        return (string) $this->product->id;
    }
}

#[UniqueFor] replaces the property:

use Illuminate\Queue\Attributes\UniqueFor;

#[UniqueFor(3600)]
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public Product $product) {}

    public function uniqueId(): string
    {
        return (string) $this->product->id;
    }
}

The argument is the lock duration in seconds. The job stays unique for one hour; a second dispatch within that window is silently discarded.

The uniqueId() method still lives on the class because it depends on the job's data. The attribute handles the configuration; the method handles the identity.


#[DebounceFor]

Debouncing is a new primitive in Laravel 13. Where ShouldBeUnique discards duplicate dispatches immediately, #[DebounceFor] defers the job and resets the delay on each new dispatch. Only the last dispatch within the window actually runs.

This is useful for jobs triggered by rapid events, like reindexing a record after user edits. Each keystroke might dispatch the job; you want only the final state to be processed.

use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)]
class UpdateSearchIndex implements ShouldQueue
{
    public function __construct(public int $productId) {}

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

With a 30-second debounce window: every new dispatch resets the delay. If dispatches stop, the job runs 30 seconds after the last one. If dispatches keep coming indefinitely, the job would never run.

maxWait prevents that:

#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
    public function __construct(public int $productId) {}

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

Now the job defers by 30 seconds on each dispatch, but is guaranteed to execute within 120 seconds of the first dispatch. Continuous writes still get batched; the index never falls more than two minutes behind.

debounceId() serves the same role as uniqueId(): it identifies which dispatches count as the same job. Two dispatches with different debounceId values are independent.


A Modern Job

All three attributes together on a job that debounces rapid dispatches, enforces uniqueness while processing, and keeps model payloads lean:

use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\DebounceFor;
use Illuminate\Queue\Attributes\UniqueFor;
use Illuminate\Queue\Attributes\WithoutRelations;

#[DebounceFor(30, maxWait: 120)]
#[UniqueFor(3600)]
class ReindexProduct implements ShouldQueue, ShouldBeUnique
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        #[WithoutRelations]
        public Product $product,
    ) {}

    public function debounceId(): string
    {
        return (string) $this->product->id;
    }

    public function uniqueId(): string
    {
        return (string) $this->product->id;
    }

    public function handle(): void
    {
        // reindex logic
    }
}

#[DebounceFor] collapses rapid dispatches into one, so only the final state gets processed. #[UniqueFor] ensures that once the job is in the queue, duplicate dispatches during the 3600-second window are ignored. #[WithoutRelations] keeps the serialized payload to just the model identifier.

All configuration is on the class. The handle() method contains only application logic.


The pattern from Eloquent models carries over cleanly. Serialization behaviour, timing constraints, and deduplication logic belong on the class definition, not buried in method implementations or constructor workarounds. If you're already using model attributes, the mental model is identical.

If you haven't seen the model attribute approach yet, the same pattern applied to Eloquent is covered in Laravel's PHP Attribute Workflow for Eloquent Models.