Skip to main content
Laravel, shipping fast.

Chapter 6

Queue & Background Processing

Julian Beaujardin

A fundamental rule is simple: Don't make the user wait for something they don't need to wait for.

You get a request to register a domain. You validate it, store it in the database, and send a confirmation email. But the user doesn't care about the email being sent right now. They just want the domain registered.

Instead of making them wait 2 seconds for the email to send, send them an immediate response: "Domain registered! You'll receive a confirmation email shortly." Then send the email in the background.

That's what queues do. They let you quickly acknowledge the request, then handle the slow parts later.

Queued Jobs

Laravel queues let you dispatch work to background workers:

Setting Up Queues

# Generate queue configuration
php artisan queue:publish

# Set queue driver
# .env
QUEUE_CONNECTION=database
# or redis, sqs, etc.

Creating Queued Jobs

Process tasks in the background:

php artisan make:job SendToLogsJob

class SendToLogsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private int $maxExceptions = 3;
    private int $backoff = 10; // seconds

    public function __construct(
        private string $method,
        private string $path,
        private int $status,
        private array $payload,
    ) {
        // Set priority (high, normal, low)
        $this->onQueue('default');
    }

    public function handle(): void
    {
        try {
            Log::channel('requests')->info('API Request', [
                'method' => $this->method,
                'path' => $this->path,
                'status' => $this->status,
                'payload' => $this->payload,
            ]);
        } catch (Exception $e) {
            $this->fail($e);
        }
    }

    // Retry with exponential backoff
    public function backoff(): array
    {
        return [10, 30, 60];
    }

    // Called when job fails after max attempts
    public function failed(Exception $exception): void
    {
        Log::error('Failed to log request', [
            'exception' => $exception::class,
            'message' => $exception->getMessage(),
            'job_data' => [
                'method' => $this->method,
                'path' => $this->path,
            ],
        ]);
    }
}

Dispatching Jobs

Queue jobs from controllers or actions:

class LogApiRequestsMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Fire-and-forget (async)
        SendToLogsJob::dispatch(
            method: $request->getMethod(),
            path: $request->getPathInfo(),
            status: $response->status(),
            payload: json_decode($response->getContent(), true),
        );

        return $response;
    }
}

// Dispatch with constraints
SendToLogsJob::dispatch($data)
    ->onQueue('logging')        // Route to specific queue
    ->delay(now()->addMinutes(5)); // Delay execution
    ->onConnection('redis');     // Use specific connection

Processing Jobs

Start queue workers to process jobs:

# Start queue worker (blocks, processes jobs as they arrive)
php artisan queue:work

# Process single job then exit (useful for cron)
php artisan queue:work --once

# Process specific queue
php artisan queue:work --queue=logging,default

# Monitor queue
php artisan queue:monitor

Job Priorities

Process important jobs first:

class HighPriorityJob implements ShouldQueue
{
    public function __construct(private string $data)
    {
        $this->onQueue('high'); // High priority queue
    }
}

class LowPriorityJob implements ShouldQueue
{
    public function __construct(private string $data)
    {
        $this->onQueue('low');
    }
}

// In worker config, process high before low
php artisan queue:work --queue=high,default,low

Handling Failures

Gracefully handle failed jobs:

class ReliableJob implements ShouldQueue, ShouldBeEncrypted
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;           // Retry 3 times
    public int $timeout = 30;        // 30 second timeout
    public int $maxExceptions = 1;   // Max exceptions before failing

    public function handle()
    {
        // If this throws after 3 tries, failed() is called
        try {
            $this->process();
        } catch (Exception $e) {
            if ($this->attempts() < $this->tries) {
                $this->release($this->backoff()); // Retry later
            } else {
                $this->fail($e);
            }
        }
    }

    public function backoff(): int
    {
        // Exponential backoff: 10s, 20s, 40s
        return 10 * (2 ** ($this->attempts() - 1));
    }

    public function failed(Exception $exception)
    {
        Notification::route('mail', 'admin@example.com')
            ->notify(new JobFailed($this, $exception));
    }

    private function process()
    {
        // Business logic
    }
}

Async Request Logging

Log all requests asynchronously:

// config/logging.php
'channels' => [
    'requests' => [
        'driver' => 'single',
        'path' => storage_path('logs/requests.log'),
        'level' => 'info',
    ],
],

// Middleware that dispatches logging job
class LogApiRequestsMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Log asynchronously (fires immediately, processed by worker)
        SendToLogsJob::dispatch(
            method: $request->getMethod(),
            path: $request->getPathInfo(),
            status: $response->status(),
            timestamp: now(),
        );

        return $response;
    }
}