Skip to content

Creating Queue Jobs

This guide walks you through creating, configuring, and testing asynchronous jobs in Laravel.

Terminal window
./vendor/bin/sail artisan make:job ProcessPayment

This 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
{
}
}
<?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(),
]);
}
}
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
}
class ProcessPayment implements ShouldQueue
{
use Queueable;
public int $timeout = 60; // 1 minute
}
class ProcessPayment implements ShouldQueue
{
use Queueable;
public string $queue = 'payments';
public string $connection = 'sqs';
}
use App\Jobs\ProcessPayment;
use App\Models\Order;
$order = Order::find(1);
ProcessPayment::dispatch($order, 'credit_card');
ProcessPayment::dispatch($order, 'credit_card')
->delay(now()->addMinutes(5));
ProcessPayment::dispatch($order, 'credit_card')
->onQueue('high-priority');
ProcessPayment::dispatchIf($order->isPaid(), $order, 'credit_card');
ProcessPayment::dispatchUnless($order->isProcessing(), $order, 'credit_card');
ProcessPayment::dispatchSync($order, 'credit_card');
ProcessPayment::dispatch($order, 'credit_card')
->afterCommit();
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(),
]);
}
public function handle(): void
{
if (!$this->order->isValid()) {
$this->fail('Order is not valid');
return;
}
}
public function handle(): void
{
if (!$this->apiIsAvailable()) {
$this->release(30); // Retry in 30 seconds
return;
}
}
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);
});
use Illuminate\Queue\Middleware\WithoutOverlapping;
public function middleware(): array
{
return [
(new WithoutOverlapping($this->order->id))
->expireAfter(180)
->dontRelease()
];
}
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;
});
});
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');
});
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();
});

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
}
}
ProcessOrderPayment::withChain([
new SendOrderConfirmation($order),
new UpdateInventory($order),
])->dispatch($order);

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);
}
use Illuminate\Contracts\Queue\ShouldBeUnique;
class ProcessPayment implements ShouldQueue, ShouldBeUnique
{
public int $orderId;
public function uniqueId(): string
{
return "payment:{$this->orderId}";
}
}
// Quick tasks
public int $timeout = 30;
// API calls
public int $timeout = 60;
// Heavy processing
public int $timeout = 3600;
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;
}
}
<?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',
]);
}
}
Terminal window
# 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=ProcessPaymentTest