Creating Queue Jobs
This guide walks you through creating, configuring, and testing asynchronous jobs in Laravel.
Basic Job Creation
Section titled “Basic Job Creation”Using Artisan
Section titled “Using Artisan”./vendor/bin/sail artisan make:job ProcessPaymentThis creates a job class at app/Jobs/ProcessPayment.php:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;
class ProcessPayment implements ShouldQueue{ use Queueable;
public function __construct() { }
public function handle(): void { }}Complete Job Structure
Section titled “Complete Job Structure”<?php
namespace App\Jobs;
use App\Models\Order;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;use Illuminate\Support\Facades\Log;use Throwable;
class ProcessOrderPayment implements ShouldQueue{ use Queueable;
public function __construct( public Order $order, public string $paymentMethod ) {}
public function handle(): void { Log::info('Processing payment', [ 'order_id' => $this->order->id, 'method' => $this->paymentMethod, ]);
// Your business logic here }
public function failed(?Throwable $exception): void { Log::error('Payment processing failed', [ 'order_id' => $this->order->id, 'error' => $exception?->getMessage(), ]); }}Job Configuration
Section titled “Job Configuration”Retry Configuration
Section titled “Retry Configuration”class ProcessPayment implements ShouldQueue{ use Queueable;
public int $tries = 3; public int $backoff = 10;}Exponential Backoff:
public function backoff(): array{ return [10, 30, 60]; // Wait 10s, 30s, then 60s}Timeout Configuration
Section titled “Timeout Configuration”class ProcessPayment implements ShouldQueue{ use Queueable;
public int $timeout = 60; // 1 minute}Queue and Connection
Section titled “Queue and Connection”class ProcessPayment implements ShouldQueue{ use Queueable;
public string $queue = 'payments'; public string $connection = 'sqs';}Dispatching Jobs
Section titled “Dispatching Jobs”Basic Dispatch
Section titled “Basic Dispatch”use App\Jobs\ProcessPayment;use App\Models\Order;
$order = Order::find(1);
ProcessPayment::dispatch($order, 'credit_card');Delayed Dispatch
Section titled “Delayed Dispatch”ProcessPayment::dispatch($order, 'credit_card') ->delay(now()->addMinutes(5));Custom Queue
Section titled “Custom Queue”ProcessPayment::dispatch($order, 'credit_card') ->onQueue('high-priority');Conditional Dispatch
Section titled “Conditional Dispatch”ProcessPayment::dispatchIf($order->isPaid(), $order, 'credit_card');ProcessPayment::dispatchUnless($order->isProcessing(), $order, 'credit_card');Synchronous Dispatch
Section titled “Synchronous Dispatch”ProcessPayment::dispatchSync($order, 'credit_card');After Database Commit
Section titled “After Database Commit”ProcessPayment::dispatch($order, 'credit_card') ->afterCommit();Error Handling
Section titled “Error Handling”Failed Method
Section titled “Failed Method”public function failed(?Throwable $exception): void{ Log::error('Job failed', [ 'job' => static::class, 'order_id' => $this->order->id, 'error' => $exception?->getMessage(), ]);
$this->order->update([ 'status' => 'payment_failed', 'error_message' => $exception?->getMessage(), ]);}Failing Explicitly
Section titled “Failing Explicitly”public function handle(): void{ if (!$this->order->isValid()) { $this->fail('Order is not valid'); return; }}Releasing Back to Queue
Section titled “Releasing Back to Queue”public function handle(): void{ if (!$this->apiIsAvailable()) { $this->release(30); // Retry in 30 seconds return; }}Job Middleware
Section titled “Job Middleware”Rate Limiting
Section titled “Rate Limiting”use Illuminate\Queue\Middleware\RateLimited;
public function middleware(): array{ return [new RateLimited('payment-api')];}Define rate limit in AppServiceProvider:
use Illuminate\Cache\RateLimiting\Limit;use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('payment-api', function (object $job) { return Limit::perMinute(30);});Without Overlapping
Section titled “Without Overlapping”use Illuminate\Queue\Middleware\WithoutOverlapping;
public function middleware(): array{ return [ (new WithoutOverlapping($this->order->id)) ->expireAfter(180) ->dontRelease() ];}Testing Jobs
Section titled “Testing Jobs”Using Queue Fake
Section titled “Using Queue Fake”use App\Jobs\ProcessPayment;use Illuminate\Support\Facades\Queue;
test('payment job is dispatched', function () { Queue::fake();
$order = Order::factory()->create(); ProcessPayment::dispatch($order, 'credit_card');
Queue::assertPushed(ProcessPayment::class, function ($job) use ($order) { return $job->order->id === $order->id; });});Testing Job Logic
Section titled “Testing Job Logic”test('payment job processes order', function () { $order = Order::factory()->create(['status' => 'pending']);
$job = new ProcessPayment($order, 'credit_card'); $job->handle();
expect($order->fresh()->status)->toBe('paid');});Testing Failed Jobs
Section titled “Testing Failed Jobs”test('payment job logs failure', function () { Log::spy();
$order = Order::factory()->create(); $job = new ProcessPayment($order, 'credit_card'); $exception = new \Exception('Payment API unavailable');
$job->failed($exception);
Log::shouldHaveReceived('error')->once();});Best Practices
Section titled “Best Practices”1. Keep Jobs Small and Focused
Section titled “1. Keep Jobs Small and Focused”Bad:
class ProcessOrder implements ShouldQueue{ public function handle(): void { // Process payment, send email, update inventory, generate invoice }}Good:
class ProcessOrderPayment implements ShouldQueue{ public function handle(): void { // Only process payment }}2. Use Job Chaining
Section titled “2. Use Job Chaining”ProcessOrderPayment::withChain([ new SendOrderConfirmation($order), new UpdateInventory($order),])->dispatch($order);3. Serialize Minimal Data
Section titled “3. Serialize Minimal Data”Bad:
public function __construct( public Collection $orders, public array $allSettings) {}Good:
public function __construct( public int $orderId) {}
public function handle(): void{ $order = Order::find($this->orderId);}4. Use Unique Jobs
Section titled “4. Use Unique Jobs”use Illuminate\Contracts\Queue\ShouldBeUnique;
class ProcessPayment implements ShouldQueue, ShouldBeUnique{ public int $orderId;
public function uniqueId(): string { return "payment:{$this->orderId}"; }}5. Set Appropriate Timeouts
Section titled “5. Set Appropriate Timeouts”// Quick taskspublic int $timeout = 30;
// API callspublic int $timeout = 60;
// Heavy processingpublic int $timeout = 3600;6. Handle External API Failures
Section titled “6. Handle External API Failures”public function handle(): void{ try { $response = Http::retry(3, 100) ->timeout(30) ->post('https://api.example.com/payment', $this->data);
if ($response->failed()) { $this->release(300); return; } } catch (RequestException $e) { $this->release(300); return; }}Complete Example
Section titled “Complete Example”<?php
namespace App\Jobs;
use App\Models\Order;use App\Services\PaymentGateway;use Illuminate\Contracts\Queue\ShouldBeUnique;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;use Illuminate\Queue\Middleware\RateLimited;use Illuminate\Queue\Middleware\WithoutOverlapping;use Illuminate\Support\Facades\Log;use Throwable;
class ProcessOrderPayment implements ShouldQueue, ShouldBeUnique{ use Queueable;
public int $tries = 3; public int $timeout = 60; public int $backoff = 30; public bool $deleteWhenMissingModels = true;
public function __construct( public int $orderId, public string $paymentMethod ) {}
public function middleware(): array { return [ new RateLimited('payment-api'), (new WithoutOverlapping($this->orderId))->expireAfter(180), ]; }
public function uniqueId(): string { return "order-payment:{$this->orderId}"; }
public function handle(PaymentGateway $gateway): void { Log::info('Processing order payment', [ 'order_id' => $this->orderId, 'attempt' => $this->attempts(), ]);
$order = Order::findOrFail($this->orderId);
if ($order->isPaid()) { Log::info('Order already paid, skipping'); return; }
try { $result = $gateway->charge($order->total, $this->paymentMethod);
$order->update([ 'status' => 'paid', 'payment_id' => $result->transactionId, 'paid_at' => now(), ]);
Log::info('Payment processed successfully'); } catch (\Exception $e) { Log::error('Payment failed', ['error' => $e->getMessage()]);
if ($this->attempts() < $this->tries) { $this->release($this->backoff); } else { $this->fail($e); } } }
public function failed(?Throwable $exception): void { Log::error('Payment job failed permanently', [ 'order_id' => $this->orderId, 'error' => $exception?->getMessage(), ]);
Order::find($this->orderId)?->update([ 'status' => 'payment_failed', ]); }}Testing Your Job
Section titled “Testing Your Job”# Dispatch via tinker./vendor/bin/sail artisan tinker>>> ProcessPayment::dispatch(1, 'credit_card');
# Monitor execution./vendor/bin/sail artisan queue:monitor
# Check logs./vendor/bin/sail artisan tail
# Run automated tests./vendor/bin/sail artisan test --filter=ProcessPaymentTestRelated Documentation
Section titled “Related Documentation”- Queue System - Queue infrastructure
- Queue Commands Reference - All available commands