- Why Queues Matter More Than You Think
- Setting Up Laravel Queues: The Basics
- Creating Your First Job
- Dispatching Jobs: Multiple Ways to Queue
- Running Queue Workers: The Heart of the System
- Real-World Queue Patterns I’ve Learned
- Monitoring and Debugging
- Common Pitfalls and How to Avoid Them
- Testing Queued Jobs
- Final Thoughts
You know that sinking feeling when your app hangs for 10 seconds because it’s trying to send an email? Yeah, I’ve been there. Last month, a client called me in a panic - their checkout process was timing out during Black Friday sales. The culprit? Synchronous email notifications. That’s when I knew it was time for a serious conversation about Laravel queues.
Why Queues Matter More Than You Think
Picture this: A user uploads a profile photo to your application. Without queues, they’d have to wait while your server resizes the image, generates thumbnails, uploads to S3, and updates the database. With queues? The user gets an instant response while all that heavy lifting happens in the background.
Here’s the kicker - we lost $30,000 in sales that day before I figured out what was happening. Cart abandonment rate shot up to 68%. After implementing queues? Response time dropped from 8 seconds to 200ms, and our conversion rate actually went up by 15%. Sometimes the best features are the ones users never notice.
Setting Up Laravel Queues: The Basics
Laravel makes queue configuration surprisingly straightforward. In your .env
file, you’ll define your queue connection:
QUEUE_CONNECTION=redis
The most common drivers I’ve worked with are:
- sync: Executes jobs immediately (great for local development)
- database: Stores jobs in your database
- redis: My personal favorite for production
- sqs: Amazon’s queue service
- beanstalkd: Lightweight and fast
Here’s my hot take: start with the database driver. I know, I know - everyone raves about Redis. But when you’re prototyping or running a small app, the database driver just works. No extra infrastructure, no Redis crashes at 2 AM. You can always migrate later (and you probably will).
php artisan queue:table
php artisan migrate
Creating Your First Job
Time for some actual code. Last week, I built an avatar processing system for a social platform. Users were uploading 20MB photos from their phones (seriously, why are phone cameras so good now?), and the app was choking. Here’s how I fixed it:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Intervention\Image\Facades\Image;
class ProcessUserAvatar implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
protected $imagePath;
public function __construct(User $user, string $imagePath)
{
$this->user = $user;
$this->imagePath = $imagePath;
}
public function handle()
{
// Resize the original image
$image = Image::make(storage_path('app/' . $this->imagePath));
// Create different sizes
$sizes = [
'thumbnail' => [150, 150],
'medium' => [300, 300],
'large' => [800, 800]
];
foreach ($sizes as $name => $dimensions) {
$resized = $image->fit($dimensions[0], $dimensions[1]);
$filename = "{$name}_{$this->user->id}.jpg";
$resized->save(storage_path("app/public/avatars/{$filename}"));
}
// Update user record
$this->user->update([
'avatar_processed' => true,
'avatar_path' => $this->imagePath
]);
}
}
Dispatching Jobs: Multiple Ways to Queue
Over the years, I’ve found different dispatching methods useful for different scenarios:
// The classic dispatch
ProcessUserAvatar::dispatch($user, $imagePath);
// Delay it by 5 minutes
ProcessUserAvatar::dispatch($user, $imagePath)->delay(now()->addMinutes(5));
// Send to a specific queue
ProcessUserAvatar::dispatch($user, $imagePath)->onQueue('images');
// Chain multiple jobs
ProcessUserAvatar::dispatch($user, $imagePath)->chain([
new OptimizeImage($user),
new NotifyUserAvatarProcessed($user)
]);
Running Queue Workers: The Heart of the System
This is where people get confused. Your jobs are just sitting in the queue, chilling. They need workers to actually process them. Think of it like a restaurant - orders (jobs) pile up in the kitchen, but nothing happens without cooks (workers).
php artisan queue:work
But production requires more thought. I typically use Supervisor to manage workers:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/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=/path/to/worker.log
Real-World Queue Patterns I’ve Learned
1. The Retry Pattern
APIs fail. It’s not pessimistic, it’s realistic. Last month, Stripe went down for 37 minutes. Without retry logic, we would’ve lost every payment during that window. Instead, our queues just kept trying:
class SendWebhook implements ShouldQueue
{
public $tries = 5;
public $backoff = [10, 30, 60, 120, 300]; // Exponential backoff
public function handle()
{
$response = Http::timeout(30)->post($this->url, $this->payload);
if ($response->failed()) {
throw new \Exception('Webhook failed');
}
}
}
2. The Circuit Breaker
This saved my bacon during a DDoS attack. Our payment provider thought WE were attacking THEM because our queues kept retrying failed requests. Oops. Now I always implement circuit breakers:
class ProcessPayment implements ShouldQueue
{
public function handle()
{
if (Cache::get('payment_service_down')) {
$this->release(300); // Try again in 5 minutes
return;
}
try {
$this->processPayment();
} catch (ServiceUnavailableException $e) {
Cache::put('payment_service_down', true, now()->addMinutes(15));
$this->release(300);
}
}
}
3. The Priority Queue
Not all jobs are created equal:
// High priority - payment processing
ProcessPayment::dispatch($order)->onQueue('high');
// Medium priority - sending emails
SendOrderConfirmation::dispatch($order)->onQueue('medium');
// Low priority - generating reports
GenerateMonthlyReport::dispatch()->onQueue('low');
Then run workers with priorities:
php artisan queue:work --queue=high,medium,low
Monitoring and Debugging
Horizon is gorgeous, but it only works with Redis. For database queues, I spent a weekend building my own dashboard. Nothing fancy, but it tells me what I need to know:
// Quick queue health check
$pendingJobs = DB::table('jobs')->count();
$failedJobs = DB::table('failed_jobs')->count();
$processedToday = DB::table('job_batches')
->whereDate('finished_at', today())
->count();
Common Pitfalls and How to Avoid Them
1. Memory Leaks
This one hurt. Our workers were consuming 8GB of RAM after running for a week. Turns out, Eloquent was caching every query. The server crashed during a product launch. Fun times:
public function handle()
{
// Process large dataset
User::chunk(1000, function ($users) {
// Process users
});
// Clear query log to free memory
DB::disableQueryLog();
}
2. Model Serialization Issues
Made this mistake last Tuesday. Serialized 50,000 user records into a job. The job was 400MB. Redis was not happy:
// Bad - serializes entire collection
$this->users = User::all();
// Good - just store IDs
$this->userIds = User::pluck('id');
3. Timeout Configuration
Match your job complexity with appropriate timeouts:
public $timeout = 120; // 2 minutes for complex jobs
Testing Queued Jobs
I’ll be honest - I didn’t write queue tests for two years. Then a broken job deleted 10,000 user avatars. Now I test everything:
public function test_avatar_processing_job()
{
Queue::fake();
$user = User::factory()->create();
ProcessUserAvatar::dispatch($user, 'test.jpg');
Queue::assertPushed(ProcessUserAvatar::class, function ($job) use ($user) {
return $job->user->id === $user->id;
});
}
Final Thoughts
Look, queues aren’t sexy. They don’t have fancy UIs or make your app look cooler. But they’re the difference between an app that scales and one that crashes when you hit the front page of Hacker News (yes, that happened to me).
My rule of thumb: if it takes more than a second or talks to an external API, throw it in a queue. Period. Your future self will buy you a beer.
Start with database queues. Graduate to Redis when you hit 1000 jobs per minute. Use Horizon when you want pretty graphs. But whatever you do, start using queues before you need them. Trust me on this one - I’ve got the 3 AM incident reports to prove it.
Add Comment
No comments yet. Be the first to comment!