Laravel Queues: How to Handle Time-Consuming Tasks in the Background

Published on November 27, 2025
Laravel Queues BackgroundJobs PHP Performance

Introduction to Laravel Queues

In modern web applications, certain tasks like sending emails, processing images, or generating reports can take significant time to complete. If handled during web requests, they can lead to poor user experience and timeouts. Laravel Queues provide a powerful solution by allowing you to defer these time-consuming tasks to be processed in the background.

Why Use Queues?

  • Improve application response times
  • Handle tasks asynchronously
  • Retry failed jobs automatically
  • Scale background processing independently
  • Provide better user experience

Common Use Cases for Queues:

  • Sending email notifications
  • Processing uploaded files/images
  • Generating PDF reports
  • Calling external APIs
  • Data analysis and aggregation
  • Social media posting
  • Database maintenance

Queue Configuration

Supported Queue Drivers

Laravel supports multiple queue drivers out of the box:

// config/queue.php

return [
    'default' => env('QUEUE_CONNECTION', 'sync'),

    'connections' => [

        'sync' => [
            'driver' => 'sync',
        ],

        'database' => [
            'driver' => 'database',
            'table' => 'jobs',
            'queue' => 'default',
            'retry_after' => 90,
            'after_commit' => false,
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
            'queue' => env('REDIS_QUEUE', 'default'),
            'retry_after' => 90,
            'block_for' => null,
            'after_commit' => false,
        ],

        'sqs' => [
            'driver' => 'sqs',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
            'queue' => env('SQS_QUEUE', 'default'),
            'suffix' => env('SQS_SUFFIX'),
            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
            'after_commit' => false,
        ],

    ],

    'failed' => [
        'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
        'database' => env('DB_CONNECTION', 'mysql'),
        'table' => 'failed_jobs',
    ],
];

Environment Configuration

# .env file
QUEUE_CONNECTION=database
REDIS_QUEUE=default

Database Driver Setup

# Create jobs table migration
php artisan queue:table

# Create failed_jobs table
php artisan queue:failed-table

# Run migrations
php artisan migrate

Creating Your First Job

Basic Job Structure

php artisan make:job ProcessPodcast

This creates app/Jobs/ProcessPodcast.php:

<?php
// app/Jobs/ProcessPodcast.php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

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

    /**
     * Create a new job instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        // Process the podcast...
    }
}

Job with Data

<?php
// app/Jobs/SendWelcomeEmail.php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

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

    public $user;
    public $tries = 3; // Number of retry attempts
    public $timeout = 60; // Timeout in seconds
    public $backoff = [10, 30, 60]; // Backoff delays between retries

    /**
     * Create a new job instance.
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        // Send welcome email
        Mail::to($this->user->email)
            ->send(new WelcomeEmail($this->user));

        // Update user record
        $this->user->update([
            'welcome_email_sent_at' => now(),
        ]);

        // Log the activity
        logger("Welcome email sent to: {$this->user->email}");
    }

    /**
     * Handle a job failure.
     */
    public function failed(\Throwable $exception): void
    {
        // Notify admin of failure
        logger()->error("Failed to send welcome email to: {$this->user->email}", [
            'error' => $exception->getMessage(),
        ]);

        // Mark user for manual follow-up
        $this->user->update([
            'needs_welcome_email' => true,
        ]);
    }
}

Dispatching Jobs

Basic Job Dispatch

<?php
// app/Http/Controllers/UserController.php

namespace App\Http\Controllers;

use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function store(Request $request)
    {
        // Validate and create user
        $user = User::create($request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]));

        // Dispatch welcome email job
        SendWelcomeEmail::dispatch($user);

        return response()->json([
            'message' => 'User created successfully',
            'user' => $user
        ], 201);
    }
}

Delayed Dispatch

<?php
// Delay job execution by 10 minutes
SendWelcomeEmail::dispatch($user)
    ->delay(now()->addMinutes(10));

// Delay based on condition
SendWelcomeEmail::dispatch($user)
    ->delay(now()->addHours($user->timezone_offset));

Specific Queue and Connection

<?php
// Dispatch to specific queue
SendWelcomeEmail::dispatch($user)
    ->onQueue('emails');

// Dispatch to specific connection and queue
SendWelcomeEmail::dispatch($user)
    ->onConnection('redis')
    ->onQueue('high-priority');

Multiple Dispatch Methods

<?php
// Different ways to dispatch jobs

// Method 1: Static dispatch
SendWelcomeEmail::dispatch($user);

// Method 2: Dispatch helper
dispatch(new SendWelcomeEmail($user));

// Method 3: Through Bus facade
use Illuminate\Support\Facades\Bus;
Bus::dispatch(new SendWelcomeEmail($user));

// Method 4: Chain multiple jobs
Bus::chain([
    new ProcessPodcast($podcast),
    new OptimizePodcast($podcast),
    new ReleasePodcast($podcast),
])->dispatch();

Real-World Job Examples

Image Processing Job

<?php
// app/Jobs/ProcessUploadedImage.php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Image;
use Intervention\Image\Facades\Image as ImageProcessor;
use Illuminate\Support\Facades\Storage;

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

    public $image;
    public $deleteWhenMissingModels = true;

    /**
     * Create a new job instance.
     */
    public function __construct(Image $image)
    {
        $this->image = $image;
        $this->onQueue('image-processing');
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        $originalPath = Storage::disk('local')->path($this->image->file_path);
        
        // Create different image sizes
        $sizes = [
            'thumbnail' => [150, 150],
            'medium' => [400, 400],
            'large' => [800, 800],
        ];

        foreach ($sizes as $sizeName => $dimensions) {
            $this->createImageSize($originalPath, $sizeName, $dimensions);
        }

        // Update image record
        $this->image->update([
            'processed_at' => now(),
            'status' => 'processed',
        ]);

        logger("Image {$this->image->id} processed successfully");
    }

    protected function createImageSize($originalPath, $sizeName, $dimensions)
    {
        [$width, $height] = $dimensions;
        
        $image = ImageProcessor::make($originalPath);
        
        // Resize maintaining aspect ratio
        $image->resize($width, $height, function ($constraint) {
            $constraint->aspectRatio();
            $constraint->upsize();
        });

        $filename = pathinfo($this->image->file_path, PATHINFO_FILENAME);
        $extension = pathinfo($this->image->file_path, PATHINFO_EXTENSION);
        
        $newPath = "images/{$sizeName}/{$filename}.{$extension}";
        
        // Save processed image
        Storage::disk('public')->put($newPath, $image->encode());
        
        // Store size information
        $this->image->sizes()->create([
            'size_name' => $sizeName,
            'file_path' => $newPath,
            'width' => $image->width(),
            'height' => $image->height(),
        ]);
    }

    /**
     * Handle job failure.
     */
    public function failed(\Throwable $exception): void
    {
        $this->image->update([
            'status' => 'failed',
            'error_message' => $exception->getMessage(),
        ]);

        logger()->error("Failed to process image {$this->image->id}", [
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString(),
        ]);
    }
}

API Synchronization Job

<?php
// app/Jobs/SyncUserDataWithCRM.php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\ConnectionException;

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

    public $user;
    public $tries = 5;
    public $maxExceptions = 3;
    public $backoff = [30, 60, 120, 300, 600]; // Exponential backoff

    /**
     * Create a new job instance.
     */
    public function __construct(User $user)
    {
        $this->user = $user;
        $this->onQueue('api-sync');
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        $response = Http::timeout(30)
            ->retry(3, 1000)
            ->withHeaders([
                'Authorization' => 'Bearer ' . config('services.crm.api_key'),
                'Content-Type' => 'application/json',
            ])
            ->post(config('services.crm.endpoint') . '/users', [
                'email' => $this->user->email,
                'name' => $this->user->name,
                'phone' => $this->user->phone,
                'company' => $this->user->company,
            ]);

        if ($response->successful()) {
            $crmData = $response->json();
            
            $this->user->update([
                'crm_id' => $crmData['id'],
                'crm_synced_at' => now(),
            ]);

            logger("User {$this->user->id} synced with CRM successfully");
        } else {
            throw new \Exception("CRM API returned error: " . $response->body());
        }
    }

    /**
     * Handle job failure.
     */
    public function failed(\Throwable $exception): void
    {
        $this->user->update([
            'crm_sync_failed' => true,
            'last_sync_attempt' => now(),
        ]);

        // Notify administrators
        if ($this->attempts() >= $this->tries) {
            $this->notifyAdmins($exception);
        }
    }

    protected function notifyAdmins(\Throwable $exception): void
    {
        $admins = User::where('is_admin', true)->get();
        
        foreach ($admins as $admin) {
            // You could dispatch an email job here
            logger()->error("CRM sync failed for user {$this->user->id} after {$this->attempts()} attempts", [
                'admin_email' => $admin->email,
                'error' => $exception->getMessage(),
                'user_id' => $this->user->id,
            ]);
        }
    }

    /**
     * Determine the time at which the job should timeout.
     */
    public function retryUntil(): \DateTime
    {
        return now()->addHours(2);
    }
}

Batch Processing Job

<?php
// app/Jobs/GenerateMonthlyReports.php

namespace App\Jobs;

use Illuminate\Bus\Batch;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use App\Models\Report;
use Illuminate\Support\Facades\Bus;
use Throwable;

class GenerateMonthlyReports implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $month;
    public $year;

    /**
     * Create a new job instance.
     */
    public function __construct($month = null, $year = null)
    {
        $this->month = $month ?? now()->subMonth()->month;
        $this->year = $year ?? now()->subMonth()->year;
        $this->onQueue('report-generation');
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        if ($this->batch()->cancelled()) {
            return;
        }

        $users = User::where('active', true)->get();
        $jobs = [];

        foreach ($users as $user) {
            $jobs[] = new GenerateUserReport($user, $this->month, $this->year);
        }

        Bus::batch($jobs)
            ->then(function (Batch $batch) {
                logger("Monthly reports batch {$batch->id} completed successfully");
            })
            ->catch(function (Batch $batch, Throwable $e) {
                logger()->error("Monthly reports batch {$batch->id} failed: " . $e->getMessage());
            })
            ->finally(function (Batch $batch) {
                logger("Monthly reports batch {$batch->id} finished processing");
            })
            ->name("Monthly Reports {$this->month}-{$this->year}")
            ->dispatch();
    }
}

// Individual user report job
class GenerateUserReport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $user;
    public $month;
    public $year;

    public function __construct(User $user, $month, $year)
    {
        $this->user = $user;
        $this->month = $month;
        $this->year = $year;
    }

    public function handle(): void
    {
        // Generate user-specific report
        $reportData = $this->generateReportData();
        
        $filename = "reports/user_{$this->user->id}_{$this->year}_{$this->month}.pdf";
        
        // Generate PDF (using a hypothetical PDF service)
        $pdfPath = PDF::generate($reportData, $filename);
        
        // Store report record
        Report::create([
            'user_id' => $this->user->id,
            'month' => $this->month,
            'year' => $this->year,
            'file_path' => $pdfPath,
            'generated_at' => now(),
        ]);

        // Notify user
        dispatch(new SendReportNotification($this->user, $pdfPath));
    }

    protected function generateReportData(): array
    {
        // Generate report data for the user
        return [
            'user' => $this->user,
            'month' => $this->month,
            'year' => $this->year,
            'statistics' => [
                'login_count' => $this->getLoginCount(),
                'orders_placed' => $this->getOrdersCount(),
                'total_spent' => $this->getTotalSpent(),
            ],
        ];
    }

    protected function getLoginCount(): int
    {
        return $this->user->loginHistories()
            ->whereYear('login_at', $this->year)
            ->whereMonth('login_at', $this->month)
            ->count();
    }

    protected function getOrdersCount(): int
    {
        return $this->user->orders()
            ->whereYear('created_at', $this->year)
            ->whereMonth('created_at', $this->month)
            ->count();
    }

    protected function getTotalSpent(): float
    {
        return $this->user->orders()
            ->whereYear('created_at', $this->year)
            ->whereMonth('created_at', $this->month)
            ->sum('total_amount');
    }
}

Queue Workers and Monitoring

Starting Queue Workers

# Basic worker
php artisan queue:work

# Specific queue
php artisan queue:work --queue=emails

# Multiple queues with priorities
php artisan queue:work --queue=high,default,low

# With specific connection
php artisan queue:work --connection=redis

# With memory and timeout limits
php artisan queue:work --memory=128 --timeout=60

# For production (daemon mode)
php artisan queue:work --daemon --sleep=3 --tries=3

Supervisor Configuration

Create /etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=/home/forge/app.com
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600

Monitoring Queues

<?php
// app/Console/Commands/MonitorQueueHealth.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Http;

class MonitorQueueHealth extends Command
{
    protected $signature = 'queue:monitor';
    protected $description = 'Monitor queue health and metrics';

    public function handle()
    {
        $metrics = $this->gatherMetrics();
        
        $this->displayMetrics($metrics);
        $this->checkThresholds($metrics);
    }

    protected function gatherMetrics(): array
    {
        return [
            'pending_jobs' => $this->getPendingJobsCount(),
            'failed_jobs' => $this->getFailedJobsCount(),
            'worker_processes' => $this->getWorkerProcessCount(),
            'queue_size' => $this->getQueueSizes(),
            'oldest_job_age' => $this->getOldestJobAge(),
        ];
    }

    protected function getPendingJobsCount(): int
    {
        return DB::table('jobs')->count();
    }

    protected function getFailedJobsCount(): int
    {
        return DB::table('failed_jobs')->count();
    }

    protected function getWorkerProcessCount(): int
    {
        $output = shell_exec('ps aux | grep "queue:work" | grep -v grep | wc -l');
        return (int) trim($output);
    }

    protected function getQueueSizes(): array
    {
        if (config('queue.default') === 'redis') {
            return [
                'default' => Redis::command('LLEN', ['queues:default']),
                'emails' => Redis::command('LLEN', ['queues:emails']),
                'high' => Redis::command('LLEN', ['queues:high']),
            ];
        }

        return [];
    }

    protected function getOldestJobAge(): ?int
    {
        $oldestJob = DB::table('jobs')
            ->orderBy('created_at', 'asc')
            ->first();

        return $oldestJob ? now()->diffInMinutes($oldestJob->created_at) : null;
    }

    protected function displayMetrics(array $metrics): void
    {
        $this->info('Queue Health Metrics:');
        $this->table(
            ['Metric', 'Value'],
            [
                ['Pending Jobs', $metrics['pending_jobs']],
                ['Failed Jobs', $metrics['failed_jobs']],
                ['Worker Processes', $metrics['worker_processes']],
                ['Oldest Job Age (minutes)', $metrics['oldest_job_age'] ?? 'N/A'],
            ]
        );

        if (!empty($metrics['queue_size'])) {
            $this->info('Queue Sizes:');
            $queueTable = [];
            foreach ($metrics['queue_size'] as $queue => $size) {
                $queueTable[] = [$queue, $size];
            }
            $this->table(['Queue', 'Size'], $queueTable);
        }
    }

    protected function checkThresholds(array $metrics): void
    {
        $alerts = [];

        if ($metrics['pending_jobs'] > 1000) {
            $alerts[] = "High pending jobs: {$metrics['pending_jobs']}";
        }

        if ($metrics['failed_jobs'] > 100) {
            $alerts[] = "High failed jobs: {$metrics['failed_jobs']}";
        }

        if ($metrics['worker_processes'] < 2) {
            $alerts[] = "Low worker count: {$metrics['worker_processes']}";
        }

        if ($metrics['oldest_job_age'] > 60) {
            $alerts[] = "Old jobs in queue: {$metrics['oldest_job_age']} minutes";
        }

        if (!empty($alerts)) {
            $this->alert('Queue Health Alerts:');
            foreach ($alerts as $alert) {
                $this->error($alert);
            }
            
            // Send notification (you could dispatch a notification job here)
            logger()->warning('Queue health alerts triggered', $alerts);
        }
    }
}

Advanced Queue Patterns

Job Middleware

<?php
// app/Jobs/Middleware/RateLimited.php

namespace App\Jobs\Middleware;

use Illuminate\Support\Facades\Redis;

class RateLimited
{
    protected $key;
    protected $allowed;
    protected $every;

    public function __construct($key = 'default', $allowed = 10, $every = 60)
    {
        $this->key = $key;
        $this->allowed = $allowed;
        $this->every = $every;
    }

    public function handle($job, $next)
    {
        Redis::throttle("job:{$this->key}")
            ->block(0)
            ->allow($this->allowed)
            ->every($this->every)
            ->then(function () use ($job, $next) {
                $next($job);
            }, function () use ($job) {
                $job->release($this->every);
            });
    }
}

// Using the middleware in a job
class CallExternalAPI implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle()
    {
        // API call logic
    }

    public function middleware()
    {
        return [new RateLimited('external-api', 5, 60)];
    }
}

Job Chains with Dependencies

<?php
// app/Http/Controllers/PodcastController.php

use Illuminate\Support\Facades\Bus;
use App\Jobs\ProcessPodcast;
use App\Jobs\OptimizePodcast;
use App\Jobs\ReleasePodcast;
use App\Jobs\NotifySubscribers;

public function publish(Podcast $podcast)
{
    Bus::chain([
        new ProcessPodcast($podcast),
        new OptimizePodcast($podcast),
        new ReleasePodcast($podcast),
        function () use ($podcast) {
            NotifySubscribers::dispatch($podcast);
            
            // Update podcast status
            $podcast->update(['published_at' => now()]);
        },
    ])->catch(function ($exception) use ($podcast) {
        // Handle any job failure in the chain
        $podcast->update(['publish_status' => 'failed']);
        logger()->error("Podcast publish chain failed: " . $exception->getMessage());
    })->dispatch();

    return response()->json(['message' => 'Podcast publishing started']);
}

Queueable Events

<?php
// app/Events/OrderPlaced.php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\Order;

class OrderPlaced implements ShouldQueue
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

// app/Listeners/SendOrderConfirmation.php

namespace App\Listeners;

use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendOrderConfirmation implements ShouldQueue
{
    public function handle(OrderPlaced $event)
    {
        // Send order confirmation email
        Mail::to($event->order->email)
            ->send(new OrderConfirmation($event->order));
    }
}

// Usage in controller
event(new OrderPlaced($order));

Testing Queues

Job Testing

<?php
// tests/Unit/Jobs/SendWelcomeEmailTest.php

namespace Tests\Unit\Jobs;

use Tests\TestCase;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SendWelcomeEmailTest extends TestCase
{
    use RefreshDatabase;

    public function test_welcome_email_is_sent()
    {
        Mail::fake();
        
        $user = User::factory()->create();
        
        // Dispatch the job
        SendWelcomeEmail::dispatch($user);
        
        // Assert email was sent
        Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
            return $mail->hasTo($user->email);
        });
    }
    
    public function test_user_is_updated_after_email_sent()
    {
        $user = User::factory()->create();
        
        SendWelcomeEmail::dispatchSync($user); // Sync for testing
        
        $this->assertNotNull($user->fresh()->welcome_email_sent_at);
    }
    
    public function test_job_is_pushed_to_queue()
    {
        Queue::fake();
        
        $user = User::factory()->create();
        
        // Dispatch job
        SendWelcomeEmail::dispatch($user);
        
        // Assert job was pushed to queue
        Queue::assertPushed(SendWelcomeEmail::class, function ($job) use ($user) {
            return $job->user->id === $user->id;
        });
    }
    
    public function test_job_failure_handling()
    {
        $user = User::factory()->create();
        
        // Mock a failure
        Mail::shouldReceive('to->send')
            ->andThrow(new \Exception('SMTP error'));
            
        $job = new SendWelcomeEmail($user);
        $job->failed(new \Exception('SMTP error'));
        
        $this->assertTrue($user->fresh()->needs_welcome_email);
    }
}

Batch Testing

<?php
// tests/Unit/Jobs/GenerateMonthlyReportsTest.php

namespace Tests\Unit\Jobs;

use Tests\TestCase;
use App\Jobs\GenerateMonthlyReports;
use App\Jobs\GenerateUserReport;
use App\Models\User;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
use Illuminate\Foundation\Testing\RefreshDatabase;

class GenerateMonthlyReportsTest extends TestCase
{
    use RefreshDatabase;

    public function test_batch_is_dispatched()
    {
        Bus::fake();
        
        User::factory()->count(5)->create(['active' => true]);
        
        // Dispatch the batch job
        GenerateMonthlyReports::dispatch();
        
        // Assert batch was dispatched
        Bus::assertBatched(function (PendingBatch $batch) {
            return $batch->jobs->count() === 5 &&
                   $batch->name === 'Monthly Reports';
        });
    }
    
    public function test_only_active_users_get_reports()
    {
        Bus::fake();
        
        $activeUsers = User::factory()->count(3)->create(['active' => true]);
        User::factory()->count(2)->create(['active' => false]);
        
        GenerateMonthlyReports::dispatch();
        
        Bus::assertBatched(function (PendingBatch $batch) use ($activeUsers) {
            return $batch->jobs->count() === 3;
        });
    }
}

Performance Optimization

Queue Configuration Tuning

// config/queue.php for high-throughput applications

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 90,
    'block_for' => 5, // Wait up to 5 seconds for jobs
    'after_commit' => false,
],

Monitoring and Scaling

# Monitor queue performance
php artisan queue:monitor

# Scale workers based on queue size
#!/bin/bash
# scale-workers.sh

QUEUE_SIZE=$(php artisan queue:size --queue=emails)
WORKER_COUNT=$(($QUEUE_SIZE / 100 + 1))

# Ensure minimum of 1, maximum of 10 workers
WORKER_COUNT=$((WORKER_COUNT < 1 ? 1 : WORKER_COUNT))
WORKER_COUNT=$((WORKER_COUNT > 10 ? 10 : WORKER_COUNT))

echo "Scaling to $WORKER_COUNT workers for queue size $QUEUE_SIZE"

# Update supervisor configuration
sed -i "s/numprocs=.*/numprocs=$WORKER_COUNT/" /etc/supervisor/conf.d/laravel-worker.conf
supervisorctl update

Common Interview Questions & Answers

  1. What are Laravel Queues and why use them?
    Laravel Queues allow you to defer time-consuming tasks to be processed in the background, improving application responsiveness and user experience.
  2. What queue drivers does Laravel support?
    Laravel supports sync, database, redis, sqs, and beanstalkd drivers out of the box.
  3. How do you handle failed jobs?
    Failed jobs are stored in the failed_jobs table. You can retry them using queue:retry or configure failure callbacks in jobs.
  4. What's the difference between queue:work and queue:listen?
    queue:work runs in daemon mode and is more efficient for production. queue:listen restarts the worker on each job, which is better for development.
  5. How do you prioritize queues?
    Use --queue=high,medium,low to specify queue priorities. Jobs in 'high' queue are processed before 'medium', etc.
  6. What are job middleware?
    Job middleware allow you to wrap custom logic around job execution, like rate limiting or logging.
  7. How do you test queued jobs?
    Use Queue::fake() to mock the queue and Bus::fake() for batches. Use dispatchSync() for synchronous testing.
  8. What's the purpose of the SerializesModels trait?
    It allows Eloquent models to be gracefully serialized and unserialized when jobs are processed, handling cases where models might be deleted.
  9. How do you monitor queue health?
    Use custom commands to check queue sizes, failed job counts, worker processes, and job ages. Integrate with monitoring services like Laravel Horizon.
  10. When should you use job batching?
    Use batching when you need to execute a large number of jobs and want to perform actions when all jobs complete, or handle failures collectively.

Troubleshooting Common Issues

Stuck Jobs

# Restart workers
php artisan queue:restart

# Clear stuck jobs
php artisan queue:flush

# Retry failed jobs
php artisan queue:retry all

Memory Issues

# Use with memory limit
php artisan queue:work --memory=128

# Process limited number of jobs
php artisan queue:work --max-jobs=100

Performance Optimization

// Use database indexing for jobs table
Schema::table('jobs', function (Blueprint $table) {
    $table->index(['queue', 'reserved_at']);
    $table->index(['queue', 'available_at']);
});

You've now mastered Laravel Queues! From basic job creation to advanced patterns like batching and middleware, you have the tools to build robust, scalable background processing for your Laravel applications.