DEV Community

Cover image for Laravel Queues Explained: Building Scalable Background Job Processing Systems
Elsayed Kamal
Elsayed Kamal

Posted on

Laravel Queues Explained: Building Scalable Background Job Processing Systems

Laravel Queues Explained: Building Scalable Background Job Processing Systems

Laravel's queue system is a game-changer for building responsive web applications. Instead of forcing users to wait for time-consuming operations like sending emails, processing images, or generating reports, Laravel queues allow you to defer these tasks to background workers. This comprehensive guide will walk you through everything you need to know about implementing and optimizing Laravel queues.

Why Laravel Queues Matter

The Problem with Synchronous Processing

Imagine a user uploading a large image to your application. Without queues, the user would have to wait while your server:

  • Validates the image
  • Resizes it to multiple formats
  • Optimizes file size
  • Uploads to cloud storage
  • Updates the database
  • Sends notification emails

This could take 30+ seconds, creating a poor user experience.

The Queue Solution

With Laravel queues, you can:

  1. Accept the upload instantly
  2. Return immediate feedback to the user
  3. Process everything in the background
  4. Notify the user when complete

Setting Up Laravel Queues

1. Choose Your Queue Driver

Laravel supports multiple queue backends:

# Database (good for development/small apps)
QUEUE_CONNECTION=database

# Redis (recommended for production)
QUEUE_CONNECTION=redis

# Amazon SQS (for AWS environments)
QUEUE_CONNECTION=sqs

# Sync (for testing - processes immediately)
QUEUE_CONNECTION=sync
Enter fullscreen mode Exit fullscreen mode

2. Database Queue Setup

For database queues, create the necessary tables:

php artisan queue:table
php artisan queue:failed-table
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

3. Redis Queue Setup

Install Redis and configure the connection:

# Install Redis
sudo apt-get install redis-server

# Install PHP Redis extension
composer require predis/predis
Enter fullscreen mode Exit fullscreen mode

Update your .env:

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Enter fullscreen mode Exit fullscreen mode

Creating and Dispatching Jobs

Basic Job Creation

Generate a new job class:

php artisan make:job ProcessImageUpload
Enter fullscreen mode Exit fullscreen mode

This creates a job class with the following structure:

<?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;

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

    protected $image;

    // Job configuration
    public $timeout = 120; // 2 minutes
    public $tries = 3; // Retry up to 3 times
    public $maxExceptions = 1; // Stop after 1 exception

    public function __construct(Image $image)
    {
        $this->image = $image;
    }

    public function handle()
    {
        // Create thumbnails
        $this->createThumbnails();

        // Optimize original image
        $this->optimizeImage();

        // Upload to cloud storage
        $this->uploadToCloud();

        // Update database
        $this->updateDatabase();

        // Send notification
        $this->notifyUser();
    }

    private function createThumbnails()
    {
        $sizes = [150, 300, 600];

        foreach ($sizes as $size) {
            $thumbnail = ImageProcessor::make($this->image->path)
                ->resize($size, $size, function ($constraint) {
                    $constraint->aspectRatio();
                    $constraint->upsize();
                });

            $thumbnailPath = storage_path("thumbnails/{$size}_{$this->image->filename}");
            $thumbnail->save($thumbnailPath, 85);
        }
    }

    private function optimizeImage()
    {
        $optimized = ImageProcessor::make($this->image->path)
            ->encode('jpg', 90);

        $optimized->save($this->image->path);
    }

    // Additional methods...
}
Enter fullscreen mode Exit fullscreen mode

Dispatching Jobs

There are several ways to dispatch jobs:

// Basic dispatch
ProcessImageUpload::dispatch($image);

// Dispatch with delay
ProcessImageUpload::dispatch($image)
    ->delay(now()->addMinutes(5));

// Dispatch to specific queue
ProcessImageUpload::dispatch($image)
    ->onQueue('image-processing');

// Conditional dispatch
ProcessImageUpload::dispatchIf($image->needs_processing, $image);
ProcessImageUpload::dispatchUnless($image->is_processed, $image);

// Chain jobs
ProcessImageUpload::withChain([
    new OptimizeImage($image),
    new GenerateMetadata($image),
    new SendNotification($image->user)
])->dispatch($image);
Enter fullscreen mode Exit fullscreen mode

Advanced Queue Features

Job Batching

Process multiple related jobs together:

use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$images = Image::where('user_id', $userId)->get();

$batch = Bus::batch(
    $images->map(fn($image) => new ProcessImageUpload($image))
)->then(function (Batch $batch) {
    // All images processed successfully
    Log::info("Batch {$batch->id} completed successfully");
})->catch(function (Batch $batch, Throwable $e) {
    // Handle batch failure
    Log::error("Batch {$batch->id} failed: " . $e->getMessage());
})->finally(function (Batch $batch) {
    // Cleanup regardless of success/failure
    Log::info("Batch {$batch->id} finished processing");
})->name('Image Processing Batch')
->allowFailures()
->dispatch();

// Check batch progress
$progress = $batch->processedPercentage();
Enter fullscreen mode Exit fullscreen mode

Job Middleware

Create reusable logic for jobs:

<?php

namespace App\Jobs\Middleware;

class RateLimited
{
    protected $key;
    protected $maxAttempts;
    protected $decayInSeconds;

    public function __construct($key, $maxAttempts = 10, $decayInSeconds = 60)
    {
        $this->key = $key;
        $this->maxAttempts = $maxAttempts;
        $this->decayInSeconds = $decayInSeconds;
    }

    public function handle($job, $next)
    {
        if (RateLimiter::tooManyAttempts($this->key, $this->maxAttempts)) {
            // Release job back to queue with delay
            $job->release(RateLimiter::availableIn($this->key));
            return;
        }

        RateLimiter::hit($this->key, $this->decayInSeconds);

        $next($job);
    }
}
Enter fullscreen mode Exit fullscreen mode

Apply middleware to jobs:

public function middleware()
{
    return [
        new RateLimited("image-processing:{$this->image->user_id}", 5, 60)
    ];
}
Enter fullscreen mode Exit fullscreen mode

Handling Failed Jobs

Implement proper error handling:

public function failed(Exception $exception)
{
    // Log the failure
    Log::error('Image processing failed', [
        'image_id' => $this->image->id,
        'error' => $exception->getMessage(),
        'trace' => $exception->getTraceAsString()
    ]);

    // Update image status
    $this->image->update(['status' => 'failed']);

    // Notify user
    $this->image->user->notify(new ImageProcessingFailed($this->image));

    // Send admin notification for critical failures
    if ($exception instanceof CriticalException) {
        Notification::route('slack', config('slack.admin_webhook'))
            ->notify(new CriticalJobFailure($this));
    }
}
Enter fullscreen mode Exit fullscreen mode

Running Queue Workers

Development

For development, use the simple worker:

# Process default queue
php artisan queue:work

# Process specific queues with priority
php artisan queue:work --queue=high,default,low

# Stop after processing 100 jobs (prevents memory leaks)
php artisan queue:work --max-jobs=100

# Stop after 1 hour
php artisan queue:work --max-time=3600
Enter fullscreen mode Exit fullscreen mode

Production with Supervisor

Create a Supervisor configuration:

[program:laravel-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --max-jobs=1000
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/queue-worker.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=5
Enter fullscreen mode Exit fullscreen mode

Start the workers:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-queue-worker:*
Enter fullscreen mode Exit fullscreen mode

Queue Monitoring and Optimization

Laravel Horizon

For Redis queues, use Laravel Horizon for monitoring:

composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Configure queues in config/horizon.php:

'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['high', 'default', 'low'],
            'balance' => 'auto',
            'maxProcesses' => 10,
            'maxTime' => 0,
            'maxJobs' => 0,
            'memory' => 512,
            'tries' => 3,
            'timeout' => 60,
            'nice' => 0,
        ],
    ],
],
Enter fullscreen mode Exit fullscreen mode

Start Horizon:

php artisan horizon
Enter fullscreen mode Exit fullscreen mode

Performance Monitoring

Track queue performance with custom metrics:

// In your job's handle method
$startTime = microtime(true);

// Your job logic here...

$duration = microtime(true) - $startTime;

// Log performance metrics
Log::info('Job performance', [
    'job' => self::class,
    'duration' => $duration,
    'memory_peak' => memory_get_peak_usage(true),
    'queue' => $this->queue,
]);

// Report to monitoring service
app('metrics')->timing('queue.job.duration', $duration, [
    'job' => self::class,
    'queue' => $this->queue,
]);
Enter fullscreen mode Exit fullscreen mode

Real-World Use Cases

Email Campaign Processing

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

    protected $campaign;
    public $timeout = 300; // 5 minutes
    public $tries = 2;

    public function __construct(EmailCampaign $campaign)
    {
        $this->campaign = $campaign;
    }

    public function handle()
    {
        $recipients = $this->campaign->getRecipients();

        // Batch recipients to avoid overwhelming the mail service
        $batches = $recipients->chunk(100);

        foreach ($batches as $batch) {
            dispatch(new SendEmailBatch($this->campaign, $batch))
                ->onQueue('email-sending')
                ->delay(now()->addSeconds(10)); // Rate limiting
        }

        $this->campaign->update(['status' => 'sending']);
    }
}
Enter fullscreen mode Exit fullscreen mode

Data Export Processing

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

    protected $reportRequest;
    public $timeout = 1800; // 30 minutes for large reports

    public function handle()
    {
        $filename = "user_report_{$this->reportRequest->id}.xlsx";
        $filePath = storage_path("reports/{$filename}");

        // Generate Excel file
        Excel::store(
            new UsersExport($this->reportRequest->filters),
            $filename,
            'reports'
        );

        // Upload to S3
        $s3Path = Storage::disk('s3')->putFile('reports', $filePath);

        // Update request with download link
        $this->reportRequest->update([
            'status' => 'completed',
            'download_url' => Storage::disk('s3')->url($s3Path),
            'expires_at' => now()->addDays(7)
        ]);

        // Notify user
        $this->reportRequest->user->notify(
            new ReportReady($this->reportRequest)
        );

        // Cleanup local file
        unlink($filePath);
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices and Tips

1. Job Design

  • Keep jobs focused on a single responsibility
  • Make jobs idempotent (safe to run multiple times)
  • Use type hints for better error reporting
  • Implement proper timeout and retry logic

2. Queue Management

  • Use different queues for different priorities
  • Monitor queue sizes and processing times
  • Implement proper logging and alerting
  • Use Redis for high-throughput applications

3. Error Handling

  • Always implement the failed() method
  • Log detailed error information
  • Implement graceful degradation
  • Set up alerts for critical failures

4. Performance Optimization

  • Use job batching for related tasks
  • Implement rate limiting for external APIs
  • Monitor memory usage and restart workers
  • Use appropriate timeout values

5. Security Considerations

  • Validate job data thoroughly
  • Use queue encryption for sensitive data
  • Implement proper access controls
  • Monitor for queue injection attacks

Testing Queue Jobs

// In your test file
use Illuminate\Support\Facades\Queue;

class ImageUploadTest extends TestCase
{
    public function test_image_upload_dispatches_processing_job()
    {
        Queue::fake();

        $user = User::factory()->create();
        $this->actingAs($user);

        $file = UploadedFile::fake()->image('test.jpg');

        $response = $this->post('/upload', ['image' => $file]);

        Queue::assertPushed(ProcessImageUpload::class, function ($job) use ($file) {
            return $job->image->filename === $file->getClientOriginalName();
        });
    }

    public function test_job_processes_image_correctly()
    {
        $image = Image::factory()->create();

        $job = new ProcessImageUpload($image);
        $job->handle();

        $this->assertTrue($image->fresh()->is_processed);
        $this->assertFileExists(storage_path("thumbnails/150_{$image->filename}"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Laravel queues are essential for building scalable, responsive applications. They allow you to:

  • Improve user experience by deferring slow operations
  • Scale your application horizontally
  • Handle failures gracefully
  • Process large volumes of background tasks

Start with database queues for development, then move to Redis for production. Always implement proper monitoring, error handling, and testing to ensure your queue system is robust and reliable.

Remember: A well-designed queue system is like a good foundation – invisible when working properly, but essential for supporting everything built on top of it.


Have you implemented Laravel queues in your projects? Share your experiences and challenges in the comments below! What queue drivers do you prefer and why?

Top comments (0)