Web applications often need to perform time-consuming tasks—sending emails, processing images, generating reports, or calling external APIs. Running these operations synchronously makes users wait and creates a poor experience. Laravel's queue system solves this elegantly by moving slow operations to background workers.
The Problem: Synchronous Processing
Consider this typical email-sending code:
public function store()
{
$order = Order::create(Input::all());
// This takes 2-3 seconds
Mail::send('emails.order-confirmation', ['order' => $order], function($message) use ($order) {
$message->to($order->customer_email);
});
return Redirect::route('orders.show', $order->id);
}
The user waits 2-3 seconds for the email to send before seeing the confirmation page. That's a poor experience.
The Solution: Queue It
With queues, the request completes immediately:
public function store()
{
$order = Order::create(Input::all());
// Queue the email - returns immediately
Queue::push('SendOrderEmail', ['order_id' => $order->id]);
return Redirect::route('orders.show', $order->id);
}
The user sees the confirmation page instantly while the email sends in the background.
Queue Drivers
Laravel supports multiple queue drivers:
Sync - Executes immediately (default, for testing) Database - Stores jobs in database (simple, no dependencies) Beanstalkd - Fast, lightweight queue server Amazon SQS - AWS managed queue service Redis - In-memory data store (our recommendation) IronMQ - Cloud-based queue service
For production, we recommend Redis for its speed and reliability.
Setting Up Redis Queue
- Install Redis:
# Mac
brew install redis
# Ubuntu
sudo apt-get install redis-server
- Install Predis package:
composer require predis/predis
- Configure queue.php:
// app/config/queue.php
return [
'default' => 'redis',
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'expire' => 60,
],
],
];
- Start queue worker:
php artisan queue:listen redis
Creating Queue Jobs
Method 1: Closure-Based
Queue::push(function($job) use ($orderId) {
$order = Order::find($orderId);
Mail::send('emails.order', ['order' => $order], function($message) use ($order) {
$message->to($order->email);
});
$job->delete();
});
Method 2: Class-Based (Recommended)
Create a job class:
<?php
class SendOrderEmail {
public function fire($job, $data)
{
$order = Order::find($data['order_id']);
Mail::send('emails.order-confirmation', ['order' => $order], function($message) use ($order) {
$message->to($order->email)
->subject('Order Confirmation #' . $order->id);
});
// Mark job as completed
$job->delete();
}
}
Queue the job:
Queue::push('SendOrderEmail', ['order_id' => $order->id]);
Delayed Jobs
Schedule jobs to run later:
// Run in 5 minutes
Queue::later(300, 'SendReminderEmail', ['user_id' => $userId]);
// Using Carbon for readability
$delay = Carbon::now()->addMinutes(10);
Queue::later($delay, 'ProcessReport', ['report_id' => $reportId]);
Job Priority
Use multiple queues for priority:
// High priority queue
Queue::pushOn('high', 'SendPasswordReset', $data);
// Low priority queue
Queue::pushOn('low', 'GenerateReport', $data);
// Normal priority (default queue)
Queue::push('SendEmail', $data);
Run workers with priority:
# Process high priority first
php artisan queue:listen redis --queue=high,default,low
Failed Jobs
Jobs can fail. Handle them gracefully:
class ProcessPayment {
public function fire($job, $data)
{
try {
$payment = Payment::process($data['payment_id']);
if ($payment->successful) {
$job->delete();
} else {
// Retry up to 3 times
if ($job->attempts() < 3) {
$job->release(60); // Try again in 60 seconds
} else {
$job->delete();
// Log failure
Log::error('Payment processing failed after 3 attempts');
}
}
} catch (Exception $e) {
$job->failed($e);
}
}
public function failed($data)
{
// Called when job fails completely
// Send alert to admin
Mail::send('emails.job-failed', $data, function($message) {
$message->to('admin@example.com');
});
}
}
Configure failed job database table:
php artisan queue:failed-table
php artisan migrate
View failed jobs:
php artisan queue:failed
Retry failed job:
php artisan queue:retry 1 # Retry job ID 1
php artisan queue:retry all # Retry all failed jobs
Real-World Use Cases
1. Email Sending
class SendWelcomeEmail {
public function fire($job, $data)
{
$user = User::find($data['user_id']);
Mail::send('emails.welcome', ['user' => $user], function($message) use ($user) {
$message->to($user->email)->subject('Welcome!');
});
$job->delete();
}
}
// Usage
Queue::push('SendWelcomeEmail', ['user_id' => $user->id]);
2. Image Processing
class ProcessProductImage {
public function fire($job, $data)
{
$product = Product::find($data['product_id']);
$imagePath = $data['image_path'];
// Generate thumbnails
$img = Image::make($imagePath);
$img->resize(800, 600)->save('uploads/large/' . basename($imagePath));
$img->resize(300, 200)->save('uploads/medium/' . basename($imagePath));
$img->resize(100, 100)->save('uploads/thumbnail/' . basename($imagePath));
$product->image = basename($imagePath);
$product->save();
$job->delete();
}
}
// Usage
Queue::push('ProcessProductImage', [
'product_id' => $product->id,
'image_path' => $uploadedFile->getPathname()
]);
3. API Calls
class SyncWithExternalAPI {
public function fire($job, $data)
{
$client = new GuzzleHttp\Client();
try {
$response = $client->post('https://api.example.com/sync', [
'json' => $data
]);
Log::info('API sync successful');
$job->delete();
} catch (Exception $e) {
Log::error('API sync failed: ' . $e->getMessage());
if ($job->attempts() < 3) {
$job->release(300); // Retry in 5 minutes
} else {
$job->delete();
}
}
}
}
4. Report Generation
class GenerateMonthlyReport {
public function fire($job, $data)
{
$month = $data['month'];
$year = $data['year'];
// Complex calculations...
$report = Report::generate($month, $year);
// Save to file
$filename = "report_{$year}_{$month}.pdf";
PDF::loadView('reports.monthly', ['data' => $report])
->save(storage_path('reports/' . $filename));
// Email to admin
Mail::send('emails.report-ready', ['filename' => $filename], function($message) {
$message->to('admin@example.com')
->subject('Monthly Report Ready')
->attach(storage_path('reports/' . $filename));
});
$job->delete();
}
}
// Schedule monthly
Queue::later(Carbon::now()->endOfMonth(), 'GenerateMonthlyReport', [
'month' => date('m'),
'year' => date('Y')
]);
Queue Workers in Production
Supervisor Configuration
Use Supervisor to keep queue workers running:
[program:laravel-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
Start Supervisor:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-queue-worker:*
Deployment
When deploying code changes, restart queue workers:
php artisan queue:restart
This gracefully finishes current jobs then restarts workers with new code.
Monitoring Queues
Check queue status:
# View queue size
redis-cli llen queues:default
# Monitor in real-time
php artisan queue:listen redis --verbose
Track job metrics:
// In AppServiceProvider::boot()
Queue::before(function ($event) {
Log::info('Queue job starting', ['job' => $event->job->getName()]);
});
Queue::after(function ($event) {
Log::info('Queue job completed', [
'job' => $event->job->getName(),
'data' => $event->data
]);
});
Best Practices
- Keep jobs small and focused - One job, one responsibility
- Make jobs idempotent - Safe to run multiple times
- Use timeouts - Prevent jobs from running forever
- Log everything - Track job execution and failures
- Monitor queue size - Alert if queues back up
- Use priorities - Critical jobs in high-priority queue
- Test with sync driver - Easier debugging during development
Performance Impact
In our production systems at ZIRA Software, implementing queues has:
- Reduced average request time from 2.3s to 180ms
- Improved user satisfaction scores by 35%
- Allowed handling of 10x more concurrent users
- Reduced server load by 40%
Conclusion
Laravel's queue system is essential for building responsive, scalable applications. By moving slow operations to background workers, you dramatically improve user experience and application performance. Whether you're sending emails, processing images, or calling external APIs, queues should be part of your Laravel application architecture.
Building a high-performance Laravel application? Contact ZIRA Software to discuss queue architecture, optimization, and scaling strategies for your project.