Introduction
Laravel 13 introduced PHP attributes for job configuration — you can now replace the old public int $tries = 3; property style with #[Tries(3)] at the class level. This pulls configuration out of the property bag and into declarative metadata, the same way controllers already use #[Route] and models use #[ObservedBy].
Key Concepts
- Job attribute: A PHP 8 attribute from
Illuminate\Queue\Attributesthat configures one aspect of a job. - Class-level only: Job attributes apply to the job class as a whole, not individual methods.
Real World Context
Properties work, but they clutter the top of every job class and mix configuration with state. Attributes make intent obvious at a glance and group cleanly with use imports the IDE can autocomplete.
Deep Dive
Compare the two styles. Here is the old property-based form:
phpclass ProcessPayment implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 5; public int $timeout = 120; public int $maxExceptions = 3; public array $backoff = [10, 30, 60]; public bool $failOnTimeout = true; }
And here is the equivalent in Laravel 13 using attributes:
phpuse Illuminate\Queue\Attributes\{Tries, Timeout, MaxExceptions, FailOnTimeout}; #[Tries(5)] #[Timeout(120)] #[MaxExceptions(3)] #[FailOnTimeout] class ProcessPayment implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public array $backoff = [10, 30, 60]; public function __construct(public Order $order) {} public function handle(PaymentGateway $gateway): void { $gateway->charge($this->order); } }
Retries, timeout, and max-exceptions become attributes; backoff stays as a property because it has no attribute equivalent yet. The job body shrinks to mostly business logic — construction and handle().
The full attribute list
| Attribute | Replaces | Purpose |
|---|---|---|
#[Tries(n)] | public int $tries | Max retry attempts |
#[Timeout(n)] | public int $timeout | Max seconds per attempt |
#[MaxExceptions(n)] | public int $maxExceptions | Fail after N exceptions |
#[FailOnTimeout] | public bool $failOnTimeout | Fail (not retry) on timeout |
#[UniqueFor(n)] | public int $uniqueFor | Uniqueness lock in seconds |
#[WithoutRelations] | deleteWhenMissingModels-adjacent | Strip loaded relations when serializing |
backoffhas no attribute form in Laravel 13 — keep it aspublic array $backoff = [...]alongside the attributes.
Mixing styles
You can still use properties where an attribute doesn't exist yet, and attributes don't affect middleware() or failed() methods — only configuration values.
Common Pitfalls
- Forgetting the
useimport — attributes look like comments without it.Illuminate\Queue\Attributes\Triesmust be imported or the class breaks silently. - Setting the same value twice — don't mix
#[Tries(5)]withpublic int $tries = 3;on the same class. The attribute wins, and the property becomes dead code that confuses readers.
Best Practices
- Adopt attributes for new jobs — keep existing jobs on properties until you touch them again, then convert in one commit so the diff is focused.
- Group the attributes above
class— one per line, sorted alphabetically, so diffs stay minimal when someone adds a new one.
Summary
- Laravel 13 adds
#[Tries],#[Timeout],#[MaxExceptions],#[Backoff],#[FailOnTimeout],#[UniqueFor], and#[WithoutRelations]. - They replace the old public property style for job configuration.
- The job body collapses to just constructor +
handle().