Introduction
Every non-trivial job needs configuration: how many retries, how long before timeout, what to do on duplicate dispatch, how to rate-limit against a flaky API. Laravel exposes this as public properties and middleware on the job class, so the config lives alongside the work instead of scattered across dispatch sites.
Key Concepts
$tries: Maximum attempts before the job is marked failed.$backoff: Seconds to wait between retries (scalar or array for exponential).$timeout: Maximum seconds a single attempt may run.ShouldBeUnique: Interface that takes a deduplication lock keyed byuniqueId().- Job middleware: Classes like
RateLimitedandWithoutOverlappingthat wraphandle()with cross-cutting logic.
Real World Context
A payment-charge job that retries 3 times with a backoff of [30, 60, 120] gives the gateway time to recover from a blip. A search-index-update job marked ShouldBeUnique means a burst of edits on the same product collapses into one re-index instead of five. A Stripe-API job wrapped in RateLimited('stripe-api') respects the 100-requests-per-second cap without you writing a single counter.
Deep Dive
Retry Configuration
phpclass ProcessPodcast implements ShouldQueue { /** * Number of times to retry. */ public int $tries = 3; /** * Maximum exceptions before failing. */ public int $maxExceptions = 3; /** * Seconds to wait before retrying. */ public int $backoff = 10; /** * Exponential backoff: 10s, 30s, 60s */ public array $backoff = [10, 30, 60]; }
Time-Based Retries
phpclass ProcessPodcast implements ShouldQueue { /** * Retry until this time. */ public function retryUntil(): DateTime { return now()->addHours(24); } }
Timeout Configuration
phpclass ProcessVideo implements ShouldQueue { /** * Job timeout in seconds. */ public int $timeout = 300; // 5 minutes /** * Fail if timeout exceeded. */ public bool $failOnTimeout = true; }
Unique Jobs
Prevent duplicate jobs:
phpuse Illuminate\Contracts\Queue\ShouldBeUnique; class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique { public function __construct( public Product $product ) {} /** * Unique identifier for the job. */ public function uniqueId(): string { return $this->product->id; } /** * Seconds until uniqueness lock expires. */ public int $uniqueFor = 3600; // 1 hour } // Only one UpdateSearchIndex for product 123 can be queued UpdateSearchIndex::dispatch($product); UpdateSearchIndex::dispatch($product); // Ignored (duplicate)
Unique Until Processing
phpuse Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; class GenerateReport implements ShouldQueue, ShouldBeUniqueUntilProcessing { // Allows another job to be queued once this one starts processing }
Rate Limiting Jobs
phpuse Illuminate\Queue\Middleware\RateLimited; use Illuminate\Support\Facades\RateLimiter; // In AppServiceProvider RateLimiter::for('api-calls', function ($job) { return Limit::perMinute(60); }); // In your job class CallExternalApi implements ShouldQueue { public function middleware(): array { return [new RateLimited('api-calls')]; } }
Preventing Overlapping Jobs
phpuse Illuminate\Queue\Middleware\WithoutOverlapping; class UpdateUserBalance implements ShouldQueue { public function __construct( public User $user ) {} public function middleware(): array { return [ new WithoutOverlapping($this->user->id), ]; } }
Release on Overlap
phppublic function middleware(): array { return [ (new WithoutOverlapping($this->user->id)) ->releaseAfter(60) // Try again in 60 seconds ->expireAfter(180), // Lock expires in 3 minutes ]; }
Job Middleware
Apply middleware to jobs:
phpuse Illuminate\Queue\Middleware\ThrottlesExceptions; class ProcessWebhook implements ShouldQueue { public function middleware(): array { return [ new ThrottlesExceptions(10, 5), // 10 exceptions per 5 minutes ]; } }
Custom Middleware
phpclass LogJobMiddleware { public function handle($job, $next) { Log::info('Starting job', ['class' => get_class($job)]); $result = $next($job); Log::info('Finished job', ['class' => get_class($job)]); return $result; } } // In your job public function middleware(): array { return [new LogJobMiddleware]; }
Common Pitfalls
- Setting
$tries = 1on a network-bound job — one blip and you're in the failed table. Use$tries >= 3with a backoff array like[10, 30, 60]for anything that touches an external API. - Forgetting
uniqueId()on aShouldBeUniquejob — without an override, every dispatch has the same lock key, and only the first job in the entire app can run. Always implementuniqueId()to scope the lock to a specific entity.
Best Practices
- Tune
$timeoutto worst-case real runtime, not happy-path — a timeout shorter than real execution creates a retry loop that never succeeds. Benchmark the job under load before setting it. - Prefer middleware over ad-hoc logic —
WithoutOverlapping,RateLimited, andThrottlesExceptionsare battle-tested. Writing your own counter inhandle()will have a race condition.
Summary
$tries,$backoff,$timeoutare public properties on the job class.ShouldBeUnique+uniqueId()deduplicates jobs by business key.middleware()returns an array of queue middleware that wrapshandle().WithoutOverlapping,RateLimited, andThrottlesExceptionscover most cross-cutting needs.