L dispatch() QUEUE SendEmail ProcessImage GenerateReport WORKER Pending Processing Done
Backend

Laravel Queue System: Background Jobs

Mayur Dabhi
Mayur Dabhi
March 25, 2026
22 min read

In modern web applications, some tasks are simply too slow to handle during a regular HTTP request. Sending emails, processing images, generating reports, or calling external APIs can take seconds or even minutes. Making users wait for these operations leads to poor user experience and timeout errors. Enter Laravel's queue system—a powerful solution for deferring time-consuming tasks to background processes.

Laravel's queue system allows you to push jobs onto a queue and process them asynchronously using worker processes. This keeps your web application fast and responsive while heavy lifting happens behind the scenes. Whether you're building a simple blog or a complex SaaS platform, understanding queues is essential for creating scalable Laravel applications.

What You'll Learn
  • Understanding queues, jobs, and workers in Laravel
  • Configuring queue drivers: Redis, Database, SQS, and more
  • Creating and dispatching jobs with best practices
  • Job batching and chaining for complex workflows
  • Rate limiting and throttling job execution
  • Handling failed jobs and implementing retry strategies
  • Monitoring queues with Laravel Horizon
  • Production deployment and scaling strategies

Understanding the Queue Architecture

Before diving into code, let's understand how Laravel's queue system works. The architecture consists of three main components: jobs (the work to be done), queues (where jobs wait), and workers (processes that execute jobs).

Laravel Queue Architecture Laravel Application Controller / Service dispatch(new Job()) Push Job Queue Storage Redis Database Amazon SQS Beanstalkd Pull Job Queue Workers Worker 1 Worker 2 Worker N Job Lifecycle Pending Processing ✓ Completed ✗ Failed Retry (if attempts left)

Jobs are dispatched to a queue storage (Redis, database, etc.), then processed by worker processes running in the background.

Jobs

Self-contained classes that define the work to be done. Each job has a handle() method containing the actual logic.

Queues

Named channels where jobs wait to be processed. You can have multiple queues with different priorities.

Workers

Long-running PHP processes that pull jobs from queues and execute them. Run via php artisan queue:work.

Drivers

Backend storage for queues: Redis (recommended), database, Amazon SQS, Beanstalkd, or sync for local dev.

Configuring Queue Drivers

Laravel supports multiple queue drivers out of the box. Your choice depends on your application's needs, infrastructure, and scale. Let's compare them:

Driver Best For Pros Cons
Redis Production apps Fast, reliable, supports delays/priorities Requires Redis server
Database Simple apps, getting started No extra services needed Slower, adds DB load
Amazon SQS AWS-hosted apps, high scale Managed, highly scalable AWS lock-in, costs
Sync Local development/testing Immediate execution Blocks request, no async

Setting Up Redis Queue

Redis is the recommended queue driver for production. Here's how to set it up:

.env
QUEUE_CONNECTION=redis

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

# Optional: Use a dedicated Redis database for queues
REDIS_QUEUE_DB=1
config/queue.php
<?php

return [
    'default' => env('QUEUE_CONNECTION', 'sync'),

    'connections' => [
        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
            'queue' => env('REDIS_QUEUE', 'default'),
            'retry_after' => 90,  // Seconds before job is retried if not acknowledged
            'block_for' => null,  // Seconds to block while waiting for jobs
            'after_commit' => false,  // Dispatch after DB transaction commits
        ],

        'database' => [
            'driver' => 'database',
            'table' => 'jobs',
            'queue' => 'default',
            'retry_after' => 90,
            'after_commit' => false,
        ],

        'sqs' => [
            'driver' => 'sqs',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
            'queue' => env('SQS_QUEUE', 'default'),
            'suffix' => env('SQS_SUFFIX'),
            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
            'after_commit' => false,
        ],
    ],

    // Define which jobs go to which queues
    'failed' => [
        'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
        'database' => env('DB_CONNECTION', 'mysql'),
        'table' => 'failed_jobs',
    ],
];
Production Tip

Set after_commit to true if your jobs depend on database records created in the same request. This ensures jobs are only dispatched after the database transaction commits successfully.

Creating and Dispatching Jobs

Jobs are the heart of Laravel's queue system. Let's create a comprehensive example that demonstrates real-world patterns.

Generating a Job Class

Terminal
# Create a basic job
php artisan make:job SendWelcomeEmail

# Create a job with the ShouldQueue interface
php artisan make:job ProcessPodcastUpload --queued

Job Class Structure

app/Jobs/SendWelcomeEmail.php
<?php

namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeEmail;
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\Mail;
use Illuminate\Support\Facades\Log;

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

    /**
     * The number of times the job may be attempted.
     */
    public int $tries = 3;

    /**
     * The number of seconds to wait before retrying the job.
     */
    public int $backoff = 30;

    /**
     * The maximum number of unhandled exceptions to allow before failing.
     */
    public int $maxExceptions = 2;

    /**
     * The number of seconds the job can run before timing out.
     */
    public int $timeout = 60;

    /**
     * Create a new job instance.
     */
    public function __construct(
        public User $user,
        public string $referralCode = ''
    ) {}

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        Log::info("Sending welcome email to {$this->user->email}");

        Mail::to($this->user->email)
            ->send(new WelcomeEmail($this->user, $this->referralCode));

        // Update user record
        $this->user->update(['welcome_email_sent_at' => now()]);

        Log::info("Welcome email sent successfully to {$this->user->email}");
    }

    /**
     * Handle a job failure.
     */
    public function failed(\Throwable $exception): void
    {
        Log::error("Failed to send welcome email to {$this->user->email}", [
            'user_id' => $this->user->id,
            'error' => $exception->getMessage(),
        ]);

        // Notify admin of the failure
        // Notification::route('slack', config('services.slack.webhook'))
        //     ->notify(new JobFailedNotification($this, $exception));
    }

    /**
     * Determine the middleware the job should pass through.
     */
    public function middleware(): array
    {
        return [
            new \Illuminate\Queue\Middleware\WithoutOverlapping($this->user->id),
        ];
    }

    /**
     * Get the tags that should be assigned to the job.
     */
    public function tags(): array
    {
        return ['email', 'welcome', 'user:' . $this->user->id];
    }
}

Dispatching Jobs

Laravel offers multiple ways to dispatch jobs to the queue:

UserController.php
use App\Jobs\SendWelcomeEmail;

// Method 1: Static dispatch
SendWelcomeEmail::dispatch($user);

// Method 2: Using dispatch() helper
dispatch(new SendWelcomeEmail($user));

// Method 3: Using the Bus facade
use Illuminate\Support\Facades\Bus;
Bus::dispatch(new SendWelcomeEmail($user));

// Dispatch with additional constructor arguments
SendWelcomeEmail::dispatch($user, 'REFERRAL2024');
Delayed Dispatch Examples
use Carbon\Carbon;

// Delay by seconds
SendWelcomeEmail::dispatch($user)->delay(60);

// Delay by minutes using Carbon
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10));

// Schedule for a specific time
SendWelcomeEmail::dispatch($user)->delay(
    Carbon::tomorrow()->setHour(9)
);

// Delay with backoff strategy (useful for rate-limited APIs)
SendWelcomeEmail::dispatch($user)
    ->delay(now()->addSeconds(30))
    ->afterCommit();  // Only dispatch after DB transaction commits
Queue-Specific Dispatch
// Dispatch to a specific queue
SendWelcomeEmail::dispatch($user)->onQueue('emails');

// Dispatch to a specific connection and queue
SendWelcomeEmail::dispatch($user)
    ->onConnection('redis')
    ->onQueue('high-priority');

// Set queue priority in the job class itself
class SendWelcomeEmail implements ShouldQueue
{
    public $queue = 'emails';  // Default queue for this job
    
    // Or dynamically set it
    public function __construct(public User $user)
    {
        $this->onQueue($user->isPremium() ? 'priority-emails' : 'emails');
    }
}
Conditional Dispatch
// Dispatch only if condition is true
SendWelcomeEmail::dispatchIf($user->wants_emails, $user);

// Dispatch unless condition is true
SendWelcomeEmail::dispatchUnless($user->email_verified, $user);

// Dispatch after response is sent to browser
SendWelcomeEmail::dispatchAfterResponse($user);

// Dispatch synchronously (bypass queue)
SendWelcomeEmail::dispatchSync($user);

// Dispatch and forget (no tracking)
dispatch(new SendWelcomeEmail($user))->afterResponse();

Job Chaining and Batching

For complex workflows, Laravel provides powerful primitives for chaining sequential jobs and batching parallel jobs.

Job Chaining vs Batching Job Chaining (Sequential) ProcessUpload GenerateThumbs NotifyUser Each job runs after the previous completes Job Batching (Parallel) Start ProcessImage 1 ProcessImage 2 ProcessImage N then() callback finally() cleanup

Chaining executes jobs in sequence; batching executes jobs in parallel with callbacks when all complete.

Job Chaining

Chain jobs when they must run in a specific order:

Job Chaining Example
<?php

use App\Jobs\ProcessPodcast;
use App\Jobs\GenerateTranscript;
use App\Jobs\NotifySubscribers;
use Illuminate\Support\Facades\Bus;

// Basic chain
Bus::chain([
    new ProcessPodcast($podcast),
    new GenerateTranscript($podcast),
    new NotifySubscribers($podcast),
])->dispatch();

// Chain with error handling
Bus::chain([
    new ProcessPodcast($podcast),
    new GenerateTranscript($podcast),
    new NotifySubscribers($podcast),
])
->onQueue('podcasts')
->onConnection('redis')
->catch(function (Throwable $e) use ($podcast) {
    // Handle chain failure
    Log::error("Podcast chain failed: {$e->getMessage()}", [
        'podcast_id' => $podcast->id
    ]);
    
    $podcast->update(['status' => 'failed']);
})
->dispatch();

// Dynamic chain based on conditions
$jobs = [new ProcessPodcast($podcast)];

if ($podcast->wants_transcript) {
    $jobs[] = new GenerateTranscript($podcast);
}

if ($podcast->notify_subscribers) {
    $jobs[] = new NotifySubscribers($podcast);
}

Bus::chain($jobs)->dispatch();

Job Batching

Batches are perfect for processing multiple items in parallel with callbacks:

Job Batching Example
<?php

use App\Jobs\ProcessImage;
use App\Models\Gallery;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

class GalleryController extends Controller
{
    public function processGallery(Gallery $gallery)
    {
        $jobs = $gallery->images->map(function ($image) {
            return new ProcessImage($image);
        })->toArray();

        $batch = Bus::batch($jobs)
            ->then(function (Batch $batch) use ($gallery) {
                // All jobs completed successfully
                $gallery->update([
                    'status' => 'processed',
                    'processed_at' => now(),
                ]);
                
                // Notify user
                $gallery->user->notify(new GalleryProcessed($gallery));
            })
            ->catch(function (Batch $batch, Throwable $e) use ($gallery) {
                // First batch job failure detected
                Log::error("Gallery processing failed", [
                    'gallery_id' => $gallery->id,
                    'error' => $e->getMessage(),
                    'failed_jobs' => $batch->failedJobs,
                ]);
            })
            ->finally(function (Batch $batch) use ($gallery) {
                // Batch finished (success or failure)
                $gallery->update([
                    'batch_id' => null,  // Clear batch reference
                    'jobs_processed' => $batch->totalJobs - $batch->pendingJobs,
                    'jobs_failed' => $batch->failedJobs,
                ]);
            })
            ->name("Process Gallery #{$gallery->id}")
            ->onQueue('image-processing')
            ->allowFailures()  // Continue processing even if some jobs fail
            ->dispatch();

        // Store batch ID for progress tracking
        $gallery->update(['batch_id' => $batch->id]);

        return response()->json([
            'message' => 'Gallery processing started',
            'batch_id' => $batch->id,
        ]);
    }

    public function checkProgress(Gallery $gallery)
    {
        $batch = Bus::findBatch($gallery->batch_id);

        if (!$batch) {
            return response()->json(['error' => 'Batch not found'], 404);
        }

        return response()->json([
            'id' => $batch->id,
            'name' => $batch->name,
            'total_jobs' => $batch->totalJobs,
            'pending_jobs' => $batch->pendingJobs,
            'failed_jobs' => $batch->failedJobs,
            'progress' => $batch->progress(),
            'finished' => $batch->finished(),
            'cancelled' => $batch->cancelled(),
        ]);
    }
}
Batching Requires Database

Job batching requires a database table to track batch status. Run php artisan queue:batches-table and php artisan migrate to create it.

Rate Limiting and Throttling

When integrating with external APIs or handling resource-intensive tasks, you often need to limit how fast jobs are processed. Laravel provides several built-in middleware and patterns for this.

Rate Limited Job
<?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\Middleware\RateLimited;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\RateLimiter;

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

    public function __construct(
        public Model $record
    ) {}

    /**
     * Get the middleware the job should pass through.
     */
    public function middleware(): array
    {
        return [
            // Global rate limit: max 60 jobs per minute across all workers
            new RateLimited('external-api'),
            
            // Throttle exceptions: if job fails, wait before retrying
            (new ThrottlesExceptions(3, 5))  // 3 attempts, 5 min cooldown
                ->backoff(5),  // Wait 5 minutes after throttling
            
            // Prevent overlapping: only one job per record at a time
            (new WithoutOverlapping($this->record->id))
                ->releaseAfter(60)  // Release lock after 60 seconds
                ->expireAfter(180), // Lock expires after 3 minutes
        ];
    }

    public function handle(): void
    {
        // Your API sync logic here
        $this->syncToApi();
    }

    /**
     * Determine the time at which the job should timeout.
     */
    public function retryUntil(): DateTime
    {
        return now()->addHours(24);  // Keep retrying for 24 hours
    }
}

// Define the rate limiter in AppServiceProvider
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        RateLimiter::for('external-api', function (object $job) {
            return Limit::perMinute(60);  // 60 jobs per minute
        });

        // Or with more sophisticated limits
        RateLimiter::for('stripe-api', function (object $job) {
            return [
                Limit::perSecond(10),      // 10 per second
                Limit::perMinute(100),     // 100 per minute  
                Limit::perHour(1000),      // 1000 per hour
            ];
        });

        // User-specific rate limiting
        RateLimiter::for('user-exports', function (object $job) {
            return Limit::perHour(5)->by($job->user->id);
        });
    }
}

Rate Limiting Strategies

  • RateLimited: Global rate limiting based on defined limiters
  • ThrottlesExceptions: Exponential backoff on repeated failures
  • WithoutOverlapping: Prevents duplicate processing of the same entity
  • Skip: Skip job execution under certain conditions

Handling Failed Jobs

Jobs can fail for many reasons: API timeouts, database deadlocks, or unexpected exceptions. Laravel provides robust tools for handling failures gracefully.

Failed Job Recovery Flow Job Fails Exception thrown Attempts Left? Yes Retry Job after backoff No failed_jobs table / storage Recovery Options • queue:retry {id} • queue:retry all • queue:flush • failed() callback
Retry Configuration Options
<?php

class ProcessPayment implements ShouldQueue
{
    // Simple retry count
    public int $tries = 5;
    
    // Exponential backoff: wait 10s, 30s, 60s between retries
    public array $backoff = [10, 30, 60];
    
    // Or use a method for dynamic backoff
    public function backoff(): array
    {
        return [1, 5, 10, 30, 60];  // Increasing delays
    }
    
    // Keep retrying until this time
    public function retryUntil(): DateTime
    {
        return now()->addHours(12);
    }
    
    // Maximum exceptions before marking as failed
    public int $maxExceptions = 3;
    
    // Timeout for the job
    public int $timeout = 120;
    
    // Delete job if model is missing (soft deletes)
    public bool $deleteWhenMissingModels = true;
    
    // Jobs that should run only once
    public bool $uniqueFor = 3600;  // Unique for 1 hour
    
    public function uniqueId(): string
    {
        return $this->payment->id;
    }
}
Handling Failures
<?php

class ProcessPayment implements ShouldQueue
{
    public function handle(): void
    {
        try {
            $this->processPaymentWithProvider();
        } catch (PaymentProviderException $e) {
            // Specific exception handling - maybe don't retry
            $this->fail($e);
            return;
        }
        // Other exceptions will trigger automatic retry
    }
    
    /**
     * Handle a job failure after all retries exhausted.
     */
    public function failed(Throwable $exception): void
    {
        // Log the failure
        Log::error('Payment processing failed permanently', [
            'payment_id' => $this->payment->id,
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString(),
        ]);
        
        // Update payment status
        $this->payment->update(['status' => 'failed']);
        
        // Notify relevant parties
        $this->payment->user->notify(new PaymentFailed($this->payment));
        
        // Alert operations team
        Notification::route('slack', config('services.slack.ops_channel'))
            ->notify(new PaymentFailedAlert($this->payment, $exception));
        
        // Create support ticket
        SupportTicket::create([
            'type' => 'payment_failure',
            'reference_id' => $this->payment->id,
            'description' => $exception->getMessage(),
        ]);
    }
}

// Global failed job handler in AppServiceProvider
use Illuminate\Support\Facades\Queue;

Queue::failing(function (JobFailed $event) {
    // Log all job failures centrally
    Log::channel('failed-jobs')->error('Job failed', [
        'connection' => $event->connectionName,
        'queue' => $event->job->getQueue(),
        'job' => $event->job->resolveName(),
        'exception' => $event->exception->getMessage(),
    ]);
});
Terminal Commands
# List all failed jobs
php artisan queue:failed

# Retry a specific failed job by ID
php artisan queue:retry 5

# Retry all failed jobs
php artisan queue:retry all

# Retry jobs that failed in the last 24 hours
php artisan queue:retry all --range=0-24

# Delete a specific failed job
php artisan queue:forget 5

# Delete all failed jobs
php artisan queue:flush

# Prune old failed jobs (keep last 48 hours)
php artisan queue:prune-failed --hours=48

# Add to scheduler for automatic cleanup
// app/Console/Kernel.php
$schedule->command('queue:prune-failed --hours=168')->daily();

Monitoring with Laravel Horizon

Laravel Horizon provides a beautiful dashboard and powerful configuration for your Redis queues. It gives you real-time insights into job throughput, runtime, and failures.

1

Install Horizon

Install via Composer and publish configuration: composer require laravel/horizon then php artisan horizon:install

2

Configure Supervisors

Define worker pools in config/horizon.php with different queues, processes, and balancing strategies.

3

Start Horizon

Run php artisan horizon to start all configured workers. Access the dashboard at /horizon.

4

Deploy with Supervisor

Use Supervisor to keep Horizon running in production and restart it after deployments.

config/horizon.php
<?php

return [
    'domain' => env('HORIZON_DOMAIN'),
    'path' => 'horizon',
    
    // Redis connection to use
    'use' => 'default',
    
    // Prefix for Horizon keys in Redis
    'prefix' => env('HORIZON_PREFIX', 'horizon:'),
    
    // Dashboard route middleware
    'middleware' => ['web', 'auth', 'can:viewHorizon'],
    
    // Queue worker configuration
    'defaults' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',      // auto, simple, or false
            'autoScalingStrategy' => 'time',  // time or size
            'maxProcesses' => 10,
            'maxTime' => 0,
            'maxJobs' => 0,
            'memory' => 128,
            'tries' => 3,
            'timeout' => 60,
            'nice' => 0,
        ],
    ],
    
    'environments' => [
        'production' => [
            // High priority jobs - always process immediately
            'supervisor-high' => [
                'connection' => 'redis',
                'queue' => ['high', 'notifications'],
                'balance' => 'false',  // Fixed number of workers
                'processes' => 5,
                'tries' => 3,
                'timeout' => 30,
            ],
            
            // Default queue with auto-scaling
            'supervisor-default' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'auto',
                'minProcesses' => 1,
                'maxProcesses' => 10,
                'balanceMaxShift' => 1,  // Max processes to add/remove at once
                'balanceCooldown' => 3,  // Seconds between scaling decisions
            ],
            
            // Low priority, long-running jobs
            'supervisor-low' => [
                'connection' => 'redis',
                'queue' => ['low', 'reports', 'exports'],
                'balance' => 'auto',
                'minProcesses' => 1,
                'maxProcesses' => 3,
                'tries' => 1,
                'timeout' => 1800,  // 30 minutes
            ],
        ],
        
        'local' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['high', 'default', 'low'],
                'balance' => 'false',
                'processes' => 3,
            ],
        ],
    ],
];
Horizon Dashboard Access

Protect your Horizon dashboard in production! Define a viewHorizon gate in AuthServiceProvider:

Gate::define('viewHorizon', function ($user) {
    return in_array($user->email, ['admin@example.com']);
});

Production Deployment

Running queues in production requires careful attention to process management, monitoring, and deployment procedures.

Supervisor Configuration

/etc/supervisor/conf.d/laravel-horizon.conf
[program:horizon]
process_name=%(program_name)s
command=php /var/www/app/artisan horizon
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/horizon.log
stopwaitsecs=3600
stopsignal=SIGTERM

; For queue:work without Horizon
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/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/app/storage/logs/worker.log
stopwaitsecs=3600

Deployment Script

deploy.sh
#!/bin/bash

# Put application in maintenance mode
php artisan down

# Pull latest code
git pull origin main

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

# Run migrations
php artisan migrate --force

# Clear and rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Restart Horizon gracefully (finish current jobs, then restart)
php artisan horizon:terminate

# Or for queue:work, restart via Supervisor
# supervisorctl restart laravel-worker:*

# Bring application back online
php artisan up

echo "Deployment complete!"
Critical: Never Kill Workers Forcefully

Always use horizon:terminate or queue:restart to gracefully stop workers. Forcefully killing workers (SIGKILL) can corrupt jobs mid-execution and leave your data in an inconsistent state.

Production Checklist

Queue Production Checklist

  • ☐ Process Manager: Supervisor or systemd keeping workers alive
  • ☐ Monitoring: Horizon dashboard or external monitoring (Datadog, NewRelic)
  • ☐ Alerting: Alerts for queue backlogs, high failure rates, and worker deaths
  • ☐ Failed Job Handling: Strategy for retrying or investigating failed jobs
  • ☐ Log Rotation: Prevent worker logs from filling up disk
  • ☐ Memory Limits: Set --memory flag to restart workers before OOM
  • ☐ Timeout Configuration: Appropriate timeouts for different job types
  • ☐ Graceful Restarts: Deployment scripts use horizon:terminate
  • ☐ Redis Persistence: Configure Redis AOF/RDB for queue durability
  • ☐ Backup Strategy: Regular backups of failed_jobs table

Best Practices and Tips

Job Design Best Practices

  • Keep jobs small and focused: One job = one responsibility. Break complex workflows into chains.
  • Make jobs idempotent: Jobs might run multiple times. Design them to produce the same result regardless.
  • Pass IDs, not objects: Serialize model IDs and refetch in handle() to get fresh data.
  • Handle missing models gracefully: Use $deleteWhenMissingModels = true or check existence.
  • Set appropriate timeouts: Know how long your job takes and set timeout accordingly.
  • Use typed properties: PHP 8+ typed properties make job payloads clear and validated.

Queue Organization Strategies

  • Separate queues by priority: high, default, low with different worker counts.
  • Separate queues by type: emails, exports, notifications for different SLAs.
  • Separate queues by resource: CPU-intensive vs I/O-bound jobs on different workers.
  • Use queue priority workers: queue:work --queue=high,default,low processes high first.

Common Pitfalls to Avoid

  • Serializing large payloads: Don't pass entire collections or large files in job constructors.
  • Not handling exceptions: Uncaught exceptions cause silent failures. Use try/catch and failed().
  • Ignoring memory leaks: Long-running workers accumulate memory. Set --max-jobs or --max-time.
  • Forgetting queue:restart: After code changes, workers still run old code until restarted.
  • Not monitoring queue depth: Backlogs can grow silently. Set up alerts for queue size.

Conclusion

Laravel's queue system is a powerful tool for building scalable, responsive applications. By offloading time-consuming tasks to background workers, you can keep your web responses fast while handling complex operations asynchronously.

Key takeaways from this guide:

With these patterns and practices, you're equipped to handle everything from simple email notifications to complex data processing pipelines. Happy queuing!

Laravel Queues Jobs Redis Horizon Background Processing
Mayur Dabhi

Mayur Dabhi

Full Stack Developer specializing in Laravel, React, and building scalable web applications. Passionate about performance optimization and clean code.