Your user just uploaded a 100MB video. Your application needs to transcode it, generate thumbnails, update the database, send notifications, and sync with a CDN. If you try to do all this synchronously, your user will be staring at a loading spinner for minutes - if the request doesn’t timeout first.

Enter Laravel Queues: the backbone of any scalable Laravel application. They transform time-consuming operations into background tasks, keeping your application responsive while heavy lifting happens behind the scenes.

This comprehensive guide reveals everything you need to know about Laravel queues - from basic job creation to advanced production deployment strategies used by applications serving millions of users.

Why Queues Transform Your Application

Consider what happens without queues:

// Without queues - User waits 30+ seconds
public function uploadVideo(Request $request)
{
    $video = $request->file('video')->store('videos');
    
    $this->transcodeVideo($video);        // 15 seconds
    $this->generateThumbnails($video);    // 5 seconds
    $this->updateSearchIndex($video);     // 2 seconds
    $this->sendNotifications($video);     // 3 seconds
    $this->syncToCDN($video);            // 5 seconds
    
    return response()->json(['status' => 'complete']); // 30+ seconds later
}

With queues, the same operation becomes:

// With queues - User waits < 1 second
public function uploadVideo(Request $request)
{
    $video = $request->file('video')->store('videos');
    
    ProcessVideo::dispatch($video);
    
    return response()->json(['status' => 'processing']); // < 1 second
}

The difference? Happy users, lower bounce rates, and the ability to handle 100x more traffic.

Understanding Queue Fundamentals

Queue Drivers: Choose Your Weapon

Laravel supports multiple queue drivers, each with distinct advantages:

# Synchronous (no queue, for development)
QUEUE_CONNECTION=sync

# Database (simple, no additional infrastructure)
QUEUE_CONNECTION=database

# Redis (fast, recommended for production)
QUEUE_CONNECTION=redis

# Amazon SQS (scalable, managed)
QUEUE_CONNECTION=sqs

# Beanstalkd (lightweight, fast)
QUEUE_CONNECTION=beanstalkd

Driver Comparison:

DriverSpeedSetupScalabilityBest For
syncN/ANoneNoneDevelopment only
databaseSlowEasyLimitedSmall apps, testing
redisFastMediumHighMost production apps
sqsFastComplexUnlimitedLarge scale, AWS stack
beanstalkdVery FastMediumHighHigh throughput

When to Use Queues

Queues are perfect for:

  • Email sending - Don’t make users wait for SMTP
  • Image/video processing - Resize, compress, transcode
  • API calls - External services, webhooks
  • Report generation - PDFs, Excel exports
  • Data imports/exports - CSV processing, batch operations
  • Notifications - Push, SMS, Slack
  • Search indexing - Elasticsearch, Algolia updates
  • Cache warming - Pre-generate expensive computations

Creating Your First Queue Job

Let’s build a real-world example - processing a podcast upload:

php artisan make:job ProcessPodcast
<?php

namespace App\Jobs;

use App\Models\Podcast;
use App\Services\AudioProcessor;
use App\Services\TranscriptionService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;

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

    public $podcast;
    public $tries = 3;
    public $timeout = 1800; // 30 minutes
    public $maxExceptions = 2;
    
    /**
     * Create a new job instance.
     */
    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }

    /**
     * Execute the job.
     */
    public function handle(
        AudioProcessor $processor,
        TranscriptionService $transcription
    ) {
        // Update status
        $this->podcast->update(['status' => 'processing']);
        
        // Process audio file
        $processedFile = $processor->normalize($this->podcast->audio_path);
        $processor->generateWaveform($processedFile);
        
        // Extract metadata
        $metadata = $processor->extractMetadata($processedFile);
        $this->podcast->update([
            'duration' => $metadata['duration'],
            'bitrate' => $metadata['bitrate'],
            'filesize' => $metadata['filesize'],
        ]);
        
        // Generate transcription
        $transcript = $transcription->transcribe($processedFile);
        $this->podcast->update(['transcript' => $transcript]);
        
        // Generate different quality versions
        $this->createAudioVersions($processedFile);
        
        // Update status
        $this->podcast->update(['status' => 'published']);
        
        // Dispatch follow-up jobs
        NotifySubscribers::dispatch($this->podcast);
        UpdatePodcastFeed::dispatch($this->podcast);
        IndexPodcastSearch::dispatch($this->podcast);
    }
    
    /**
     * Handle job failure.
     */
    public function failed(\Throwable $exception)
    {
        $this->podcast->update(['status' => 'failed']);
        
        // Notify admin
        \Log::error('Podcast processing failed', [
            'podcast_id' => $this->podcast->id,
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString(),
        ]);
        
        // Clean up temporary files
        Storage::delete($this->podcast->temp_files);
    }
    
    /**
     * Determine the time at which the job should timeout.
     */
    public function retryUntil()
    {
        return now()->addHours(2);
    }
    
    private function createAudioVersions($file)
    {
        $versions = [
            'high' => ['bitrate' => '320k', 'suffix' => '_high'],
            'medium' => ['bitrate' => '192k', 'suffix' => '_medium'],
            'low' => ['bitrate' => '128k', 'suffix' => '_low'],
        ];
        
        foreach ($versions as $quality => $settings) {
            // Process each version
            // This could also be separate jobs
        }
    }
}

Dispatching Jobs with Options

// Basic dispatch
ProcessPodcast::dispatch($podcast);

// Delay execution
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));

// Dispatch to specific queue
ProcessPodcast::dispatch($podcast)->onQueue('podcasts');

// Set priority (if driver supports it)
ProcessPodcast::dispatch($podcast)->onQueue('high-priority');

// Dispatch conditionally
ProcessPodcast::dispatchIf($podcast->needs_processing, $podcast);

// Dispatch after response sent
ProcessPodcast::dispatchAfterResponse($podcast);

// Dispatch synchronously (bypass queue)
ProcessPodcast::dispatchSync($podcast);

// Dispatch with custom connection
ProcessPodcast::dispatch($podcast)->onConnection('sqs');

Queue Configuration Deep Dive

Database Queue Setup

php artisan queue:table
php artisan queue:failed-table
php artisan migrate

The database driver creates these tables:

  • jobs - Pending jobs
  • failed_jobs - Failed job records

Redis Configuration for Queues

// config/database.php
'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'), // phpredis is faster
    
    'default' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_DB', '0'),
    ],
    
    'queue' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', '6379'),
        'database' => '1', // Use different database for queues
        'persistent' => true, // Keep connection alive
    ],
],

// config/queue.php
'redis' => [
    'driver' => 'redis',
    'connection' => 'queue',
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 90,
    'block_for' => 5, // Long polling for efficiency
    'after_commit' => false,
],

Multiple Queue Configuration

// config/queue.php
'connections' => [
    'critical' => [
        'driver' => 'redis',
        'connection' => 'queue',
        'queue' => 'critical',
        'retry_after' => 60,
    ],
    
    'default' => [
        'driver' => 'redis',
        'connection' => 'queue',
        'queue' => 'default',
        'retry_after' => 90,
    ],
    
    'bulk' => [
        'driver' => 'redis',
        'connection' => 'queue',
        'queue' => 'bulk',
        'retry_after' => 300,
    ],
],

Running Queue Workers Like a Pro

Development Environment

# Basic worker - processes jobs one by one
php artisan queue:work

# Watch for code changes and restart
php artisan queue:work --tries=1 --timeout=60

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

# Verbose output for debugging
php artisan queue:work --verbose

Production Worker Command

php artisan queue:work redis \
  --queue=critical,high,default,low \
  --sleep=3 \
  --tries=3 \
  --timeout=90 \
  --memory=512 \
  --max-jobs=1000 \
  --max-time=3600 \
  --rest=0.5

Options Explained:

  • --queue: Process queues in priority order
  • --sleep: Seconds to sleep when no jobs available
  • --tries: Maximum attempts before failing
  • --timeout: Maximum seconds per job
  • --memory: Maximum memory in MB
  • --max-jobs: Jobs to process before restarting
  • --max-time: Seconds to run before restarting
  • --rest: Seconds to rest between jobs

Worker Lifecycle Management

// app/Console/Commands/QueueHealthCheck.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Cache;

class QueueHealthCheck extends Command
{
    protected $signature = 'queue:health';
    protected $description = 'Check queue worker health';
    
    public function handle()
    {
        $queues = ['critical', 'default', 'low'];
        $unhealthy = [];
        
        foreach ($queues as $queue) {
            $size = Queue::size($queue);
            $lastProcessed = Cache::get("queue:{$queue}:last_processed");
            
            if ($size > 1000) {
                $unhealthy[] = "{$queue}: {$size} jobs backed up";
            }
            
            if ($lastProcessed && $lastProcessed->diffInMinutes() > 5) {
                $unhealthy[] = "{$queue}: No jobs processed in 5 minutes";
            }
        }
        
        if (count($unhealthy) > 0) {
            $this->error('Queue health check failed:');
            foreach ($unhealthy as $issue) {
                $this->error("  - {$issue}");
            }
            
            // Send alert
            return 1;
        }
        
        $this->info('All queues healthy');
        return 0;
    }
}

Supervisor Configuration for Production

Supervisor ensures your queue workers stay running:

sudo apt-get install supervisor

Basic Supervisor Configuration

# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourapp/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/worker.log
stopwaitsecs=3600
startsecs=0

Advanced Multi-Queue Configuration

# /etc/supervisor/conf.d/laravel-queues.conf

# Critical queue - more workers, higher priority
[program:laravel-critical]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourapp/artisan queue:work redis --queue=critical --timeout=60 --memory=256 --tries=2
priority=10
numprocs=4
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/critical.log

# Default queue - balanced configuration
[program:laravel-default]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourapp/artisan queue:work redis --queue=high,default --timeout=90 --memory=512 --tries=3
priority=20
numprocs=6
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/default.log

# Bulk queue - fewer workers, longer timeout
[program:laravel-bulk]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourapp/artisan queue:work redis --queue=bulk,low --timeout=300 --memory=1024 --tries=1
priority=30
numprocs=2
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/bulk.log

# Group all Laravel workers
[group:laravel]
programs=laravel-critical,laravel-default,laravel-bulk

Managing Supervisor

# Reload configuration
sudo supervisorctl reread
sudo supervisorctl update

# Start all Laravel workers
sudo supervisorctl start laravel:*

# Stop all Laravel workers
sudo supervisorctl stop laravel:*

# Restart all Laravel workers
sudo supervisorctl restart laravel:*

# Check status
sudo supervisorctl status

# Monitor logs
tail -f /var/www/yourapp/storage/logs/worker.log

Laravel Horizon: Queue Management on Steroids

Horizon provides a beautiful dashboard and auto-balancing for Redis queues:

Installation and Setup

composer require laravel/horizon
php artisan horizon:install

Horizon Configuration

// config/horizon.php
return [
    'use' => 'default',
    
    'prefix' => env('HORIZON_PREFIX', 'horizon:'),
    
    'waits' => [
        'redis:default' => 60,
    ],
    
    'trim' => [
        'recent' => 60,
        'pending' => 60,
        'completed' => 60,
        'recent_failed' => 10080,
        'failed' => 10080,
        'monitored' => 10080,
    ],
    
    'metrics' => [
        'trim_snapshots' => [
            'job' => 24,
            'queue' => 24,
        ],
    ],
    
    'fast_termination' => false,
    
    'memory_limit' => 64,
    
    'environments' => [
        'production' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['critical', 'default'],
                'balance' => 'auto',
                'autoScalingStrategy' => 'time',
                'maxProcesses' => 10,
                'minProcesses' => 1,
                'balanceMaxShift' => 1,
                'balanceCooldown' => 3,
                'memory' => 512,
                'tries' => 3,
                'nice' => 0,
            ],
            
            'supervisor-2' => [
                'connection' => 'redis',
                'queue' => ['bulk', 'low'],
                'balance' => 'simple',
                'maxProcesses' => 5,
                'minProcesses' => 1,
                'memory' => 1024,
                'tries' => 1,
                'timeout' => 300,
                'nice' => 0,
            ],
        ],
        
        'local' => [
            'supervisor-1' => [
                'maxProcesses' => 3,
                'autoScalingStrategy' => 'time',
                'balanceMaxShift' => 1,
                'balanceCooldown' => 3,
            ],
        ],
    ],
];

Horizon Supervisor Configuration

# /etc/supervisor/conf.d/horizon.conf
[program:horizon]
process_name=%(program_name)s
command=php /var/www/yourapp/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/horizon.log
stopwaitsecs=3600

Horizon Monitoring

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // Capture Horizon metrics
    $schedule->command('horizon:snapshot')->everyFiveMinutes();
    
    // Monitor Horizon health
    $schedule->call(function () {
        if (! Horizon::allMasterSupervisorsRunning()) {
            // Alert administrators
            Mail::to('admin@example.com')->send(new HorizonDown());
        }
    })->everyMinute();
}

Handling Failed Jobs

Failed Job Configuration

// app/Jobs/ProcessOrder.php
class ProcessOrder implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 30, 60]; // Exponential backoff
    
    public function handle()
    {
        // Job logic
    }
    
    public function failed(Throwable $exception)
    {
        // Log failure
        Log::error('Order processing failed', [
            'order_id' => $this->order->id,
            'error' => $exception->getMessage(),
        ]);
        
        // Update order status
        $this->order->update(['status' => 'failed']);
        
        // Notify customer service
        CustomerService::notifyFailedOrder($this->order);
        
        // Store failure details
        FailedOrderProcessing::create([
            'order_id' => $this->order->id,
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString(),
        ]);
    }
    
    /**
     * Determine if the job should be retried.
     */
    public function shouldRetry(Throwable $exception)
    {
        // Don't retry validation errors
        if ($exception instanceof ValidationException) {
            return false;
        }
        
        // Retry network errors
        if ($exception instanceof RequestException) {
            return true;
        }
        
        return false;
    }
}

Managing Failed Jobs

# View all failed jobs
php artisan queue:failed

# Retry specific failed job
php artisan queue:retry 5e3f6c8e-8b9a-4f7d-9c2e-1a3b5c7d9e1f

# Retry all failed jobs
php artisan queue:retry all

# Retry failed jobs for specific queue
php artisan queue:retry --queue=default

# Delete a failed job
php artisan queue:forget 5e3f6c8e-8b9a-4f7d-9c2e-1a3b5c7d9e1f

# Flush all failed jobs
php artisan queue:flush

Custom Failed Job Handler

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Queue;

public function boot()
{
    Queue::failing(function (JobFailed $event) {
        // $event->connectionName
        // $event->job
        // $event->exception
        
        // Send to error tracking
        app('sentry')->captureException($event->exception, [
            'extra' => [
                'job' => $event->job->getName(),
                'queue' => $event->job->getQueue(),
                'payload' => $event->job->payload(),
            ],
        ]);
        
        // Store metrics
        Cache::increment('failed_jobs_count');
        Cache::put('last_failed_job', now());
    });
}

Production Best Practices

Memory Management

// app/Jobs/LongRunningJob.php
class LongRunningJob implements ShouldQueue
{
    public function handle()
    {
        // Process large dataset in chunks
        User::chunk(1000, function ($users) {
            foreach ($users as $user) {
                $this->processUser($user);
            }
            
            // Clear Eloquent's internal cache
            User::flushEventListeners();
            User::clearBootedModels();
            
            // Force garbage collection
            gc_collect_cycles();
        });
    }
}

Zero-Downtime Deployment

#!/bin/bash
# deploy.sh - Graceful queue deployment

echo "Starting deployment..."

# Put application in maintenance mode
php artisan down --message="Upgrading, back in a minute" --retry=60

# Pull latest code
git pull origin main

# Install dependencies
composer install --no-dev --optimize-autoloader

# Run migrations
php artisan migrate --force

# Clear caches
php artisan cache:clear
php artisan config:cache
php artisan route:cache

# Gracefully restart queue workers
php artisan queue:restart

# If using Horizon
php artisan horizon:terminate

# Wait for workers to finish current jobs
sleep 10

# Restart Supervisor
sudo supervisorctl restart laravel:*

# Bring application back online
php artisan up

echo "Deployment complete!"

Queue Monitoring and Alerts

// app/Console/Commands/MonitorQueues.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Mail;
use App\Mail\QueueAlert;

class MonitorQueues extends Command
{
    protected $signature = 'queue:monitor';
    protected $description = 'Monitor queue health';
    
    private $thresholds = [
        'critical' => 100,
        'default' => 500,
        'bulk' => 2000,
    ];
    
    public function handle()
    {
        $alerts = [];
        
        foreach ($this->thresholds as $queue => $threshold) {
            $size = Queue::size($queue);
            
            if ($size > $threshold) {
                $alerts[] = "Queue '{$queue}' has {$size} jobs (threshold: {$threshold})";
            }
            
            // Check for stuck jobs
            $oldestJob = DB::table('jobs')
                ->where('queue', $queue)
                ->orderBy('created_at')
                ->first();
            
            if ($oldestJob && Carbon::parse($oldestJob->created_at)->diffInMinutes() > 30) {
                $alerts[] = "Queue '{$queue}' has jobs older than 30 minutes";
            }
        }
        
        // Check failed jobs
        $failedCount = DB::table('failed_jobs')->count();
        if ($failedCount > 50) {
            $alerts[] = "Failed jobs table has {$failedCount} entries";
        }
        
        if (count($alerts) > 0) {
            Mail::to('ops@example.com')->send(new QueueAlert($alerts));
            
            foreach ($alerts as $alert) {
                $this->error($alert);
            }
            
            return 1;
        }
        
        $this->info('All queues healthy');
        return 0;
    }
}

// Schedule in Kernel.php
$schedule->command('queue:monitor')->everyFiveMinutes();

Advanced Queue Patterns

Job Chaining

// Chain multiple jobs
ProcessPodcast::withChain([
    new OptimizePodcast($podcast),
    new PublishPodcast($podcast),
    new NotifySubscribers($podcast),
])->dispatch($podcast);

// Conditional chaining
$chain = [
    new ProcessVideo($video),
];

if ($video->needs_transcoding) {
    $chain[] = new TranscodeVideo($video);
}

if ($video->auto_publish) {
    $chain[] = new PublishVideo($video);
}

ProcessVideo::withChain($chain)->dispatch($video);

Job Batching

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

$batch = Bus::batch([
    new ImportCsv($file, 0, 1000),
    new ImportCsv($file, 1000, 2000),
    new ImportCsv($file, 2000, 3000),
])->then(function (Batch $batch) {
    // All jobs completed successfully
    Mail::to('admin@example.com')->send(new ImportComplete());
})->catch(function (Batch $batch, Throwable $e) {
    // First batch job failure detected
    Mail::to('admin@example.com')->send(new ImportFailed($e));
})->finally(function (Batch $batch) {
    // The batch has finished executing
    Storage::delete($file);
})->dispatch();

// Track batch progress
$batch = Bus::findBatch($batchId);
echo $batch->progress(); // 66 (percentage)
echo $batch->totalJobs; // 3
echo $batch->pendingJobs; // 1
echo $batch->failedJobs; // 0

Rate Limiting

use Illuminate\Support\Facades\RateLimiter;

class SendMarketingEmail implements ShouldQueue
{
    public function handle()
    {
        RateLimiter::attempt(
            'send-email:' . $this->user->id,
            $maxAttempts = 5,
            function () {
                // Send email
                Mail::to($this->user)->send(new MarketingEmail());
            },
            $decaySeconds = 60
        );
    }
    
    public function middleware()
    {
        return [
            new RateLimited('emails'), // Global rate limit
        ];
    }
}

Unique Jobs

use Illuminate\Contracts\Queue\ShouldBeUnique;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public $uniqueFor = 3600; // Unique for 1 hour
    
    public function uniqueId()
    {
        return $this->product->id;
    }
    
    public function handle()
    {
        // Only one job per product ID will run
    }
}

Common Issues and Solutions

Jobs Not Processing

# Check if workers are running
ps aux | grep "queue:work"

# Check Redis connection
redis-cli ping

# Check for jobs in queue
php artisan queue:size default

# Manually process a job
php artisan queue:work --once

# Check logs
tail -f storage/logs/laravel.log
tail -f storage/logs/worker.log

Memory Exhaustion

// Limit memory per job
class HeavyJob implements ShouldQueue
{
    public function handle()
    {
        ini_set('memory_limit', '1024M');
        
        // Process in smaller chunks
        collect($this->data)->chunk(100)->each(function ($chunk) {
            $this->processChunk($chunk);
            
            // Clear memory
            unset($chunk);
            gc_collect_cycles();
        });
    }
}

Timeout Issues

class LongRunningJob implements ShouldQueue
{
    public $timeout = 1800; // 30 minutes
    
    public function handle()
    {
        // For really long jobs, disable timeout
        set_time_limit(0);
        
        // Or increase dynamically
        ini_set('max_execution_time', 3600);
    }
}

Database Deadlocks

use Illuminate\Database\DeadlockException;

class UpdateInventory implements ShouldQueue
{
    public $tries = 5;
    
    public function handle()
    {
        DB::transaction(function () {
            // Lock records in consistent order to prevent deadlocks
            $products = Product::whereIn('id', $this->productIds)
                ->orderBy('id')
                ->lockForUpdate()
                ->get();
            
            foreach ($products as $product) {
                $product->decrement('quantity');
            }
        }, 5); // Retry transaction 5 times on deadlock
    }
    
    public function retryUntil()
    {
        return now()->addMinutes(10);
    }
}

Performance Optimization for Queues

Queue Priority Strategy

// Dispatch to different queues based on importance
if ($order->value > 1000) {
    ProcessOrder::dispatch($order)->onQueue('critical');
} elseif ($order->is_premium_customer) {
    ProcessOrder::dispatch($order)->onQueue('high');
} else {
    ProcessOrder::dispatch($order)->onQueue('default');
}

// Process queues with priority
php artisan queue:work --queue=critical,high,default,low

Redis Optimization

-- Lua script for atomic operations
local key = KEYS[1]
local job = ARGV[1]
local score = ARGV[2]

if redis.call('ZRANK', key, job) then
    return 0
else
    return redis.call('ZADD', key, score, job)
end

Database Queue Optimization

-- Add indexes for better performance
ALTER TABLE jobs ADD INDEX queue_reserved_at_index (queue, reserved_at);
ALTER TABLE failed_jobs ADD INDEX failed_at_index (failed_at);

-- Clean old jobs periodically
DELETE FROM jobs WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 DAY);
DELETE FROM failed_jobs WHERE failed_at < DATE_SUB(NOW(), INTERVAL 30 DAY);

The Future of Queue Processing

As applications scale, queue management becomes increasingly critical. Modern platforms are moving towards:

  • Auto-scaling workers based on queue depth
  • Serverless job processing with AWS Lambda or Google Cloud Functions
  • Event-driven architectures with Kafka or RabbitMQ
  • Distributed tracing for complex job chains

Modern server automation tools can handle queue worker management automatically, with intelligent auto-scaling, built-in monitoring, and zero-configuration deployment for Laravel applications using the techniques outlined in this guide.

Conclusion

Laravel queues transform time-consuming operations into smooth background processes, dramatically improving user experience and application scalability. We’ve covered:

  1. Queue fundamentals - Understanding drivers and use cases
  2. Job creation - Building robust, failure-resistant jobs
  3. Worker management - Running and monitoring workers in production
  4. Supervisor & Horizon - Keeping workers running reliably
  5. Failed job handling - Graceful failure recovery
  6. Advanced patterns - Chaining, batching, and rate limiting
  7. Production best practices - Memory management, monitoring, and optimization

With these techniques, you can handle millions of jobs reliably, scale horizontally, and maintain sub-second response times even under heavy load.

Remember: queues aren’t just about performance - they’re about building resilient, scalable applications that can grow with your business.

By implementing the queue worker management techniques in this guide on your Ubuntu servers, you can achieve enterprise-grade queue processing with automatic scaling, comprehensive monitoring, and production-ready reliability.