Navigation

Laravel

Laravel Queue System: Asynchronous Job Management

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 1. The Retry Pattern 2. The Circuit Breaker 3. The Priority Queue Mon...
Jul 04, 2025
7 min read

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.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel