Programming Intermediate 12 min

How to Run Background Jobs with Laravel Queues

Anything that does not need to happen before you can respond to the user belongs in a queue. Sending a welcome email, resizing an uploaded image, calling a third-party API, generating a PDF — none of these should block the HTTP response. Queues move that work to a background worker, keeping your response times low and your retries automatic.

Laravel's queue system is one of its best features. A unified API hides whether the driver is a database table, Redis, SQS, or something else. This guide uses the database driver — zero extra infrastructure, works everywhere.

Step-by-step

  1. 1

    Configure the Queue Driver

    Set QUEUE_CONNECTION=database in your .env file. Then generate the jobs table migration and run it. This creates the jobs and failed_jobs tables where your queue entries will live.

    bash
    # .env
    QUEUE_CONNECTION=database
    
    # Generate and run the migrations
    php artisan queue:table
    php artisan queue:failed-table
    php artisan migrate
  2. 2

    Create a Job Class

    Generate a job with make:job. The class gets a handle() method where the work happens. Inject any dependencies directly — the service container resolves them when the worker picks up the job.

    bash
    php artisan make:job SendWelcomeEmail
  3. 3

    Implement the Job

    Implement ShouldQueue to tell Laravel this job is queued (not synchronous). Store only what you need in the constructor — typically just the model or a small value object. Avoid storing large arrays or full request objects; they bloat the serialized job payload.

    php
    <?php
    
    namespace App\Jobs;
    
    use App\Mail\WelcomeMail;
    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 Illuminate\Support\Facades\Mail;
    
    class SendWelcomeEmail implements ShouldQueue
    {
        use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
        public int $tries = 3;          // Retry up to 3 times
        public int $backoff = 60;       // Wait 60s between retries
    
        public function __construct(
            public readonly User $user
        ) {}
    
        public function handle(): void
        {
            Mail::to($this->user)->send(new WelcomeMail($this->user));
        }
    
        public function failed(\Throwable $exception): void
        {
            // Notify the team, log, or clean up
            logger()->error('SendWelcomeEmail failed', [
                'user_id' => $this->user->id,
                'error'   => $exception->getMessage(),
            ]);
        }
    }
  4. 4

    Dispatch the Job

    Dispatch from anywhere — a controller, a service, an event listener. The job is serialized, written to the jobs table, and picked up by the worker asynchronously. Use dispatch() for immediate queuing or dispatch()->delay() to schedule it for later.

    php
    use App\Jobs\SendWelcomeEmail;
    
    // In a controller after registration
    SendWelcomeEmail::dispatch($user);
    
    // Delay by 5 minutes
    SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
    
    // Send to a specific queue (useful for prioritisation)
    SendWelcomeEmail::dispatch($user)->onQueue('emails');
    
    // Dispatch only if condition is met
    SendWelcomeEmail::dispatchIf($user->wants_email, $user);
  5. 5

    Run the Queue Worker

    The worker process pulls jobs from the queue and runs them. Use queue:work in development. It processes jobs continuously until you stop it. For production, use a process manager — Supervisor ensures the worker restarts automatically if it crashes.

    bash
    # Development
    php artisan queue:work
    
    # Process a specific queue with timeout and memory limit
    php artisan queue:work --queue=emails,default --timeout=60 --memory=128
    
    # Process a single job then exit (useful in cron-based setups)
    php artisan queue:work --once
  6. 6

    Configure Supervisor for Production

    Supervisor keeps the worker process running on the server. Create a config file in /etc/supervisor/conf.d/. The numprocs setting controls how many worker processes run in parallel — scale it based on your job volume and server resources.

    ini
    [program:laravel-worker]
    process_name=%(program_name)s_%(process_num)02d
    command=php /var/www/esb1995.com/artisan queue:work database --sleep=3 --tries=3 --timeout=90
    autostart=true
    autorestart=true
    stopasgroup=true
    killasgroup=true
    user=apache
    numprocs=2
    redirect_stderr=true
    stdout_logfile=/var/log/worker.log
    stopwaitsecs=3600
  7. 7

    Monitor and Retry Failed Jobs

    When a job exceeds its $tries limit, it lands in the failed_jobs table. Use the Artisan commands to inspect and retry. On production, consider setting up alerts when the failed jobs count grows — it means something upstream is broken.

    bash
    # List all failed jobs
    php artisan queue:failed
    
    # Retry a specific failed job by its UUID
    php artisan queue:retry 5
    
    # Retry all failed jobs
    php artisan queue:retry all
    
    # Delete a specific failed job
    php artisan queue:forget 5
    
    # Delete all failed jobs
    php artisan queue:flush
  8. 8

    Restart Workers After Deploy

    Workers load your application code at startup. If you deploy new code without restarting workers, the running processes will keep using the old code. The safe pattern is to signal a restart after every deploy — the worker finishes its current job, then exits cleanly so Supervisor restarts it with the new code.

    bash
    # Add this to your deploy script after git pull + optimize
    php artisan queue:restart
    
    # Supervisor will automatically start fresh workers with new code
    # (queue:restart signals via the cache — ensure your cache driver is shared)

Tips & gotchas

  • Use named queues to prioritise work: <code>--queue=critical,high,default</code>. The worker drains <code>critical</code> completely before touching <code>high</code>. Put payment processing on <code>critical</code>, emails on <code>default</code>.
  • Set <code>$timeout</code> on the job class to kill runaway jobs. Without it, a stuck job holds a worker slot forever.
  • Avoid storing full Eloquent collections in a job constructor. Retrieve what you need inside <code>handle()</code> — the data may have changed between dispatch and execution anyway.
  • <code>queue:work</code> loads your app once and keeps it in memory. Code changes are not picked up until the worker restarts. Always run <code>queue:restart</code> after deploy.
  • For local development, set <code>QUEUE_CONNECTION=sync</code> in <code>.env</code> — jobs run immediately without needing a worker, making debugging much easier.

Wrapping up

Queues are not an optimisation — they are the correct architecture for any work that does not need to block a response. Get the database driver working locally, then switch to Redis in production for better throughput.

#Laravel #Queues #Background
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.