Reader Stacks

How to Run Background Queue Jobs in Laravel

Dispatching a job pushes it onto a queue instead of running it inline — the request returns to the user immediately, while a separate queue worker process actually does the slow work.

A queued job moves slow work — sending an email, processing an uploaded video, calling a slow third-party API — off the main request cycle entirely, letting the user's request return immediately while a separate background worker process handles the actual work whenever it gets to it.

Creating a job

php artisan make:job ProcessVideoUpload
class ProcessVideoUpload implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public Video $video) {}

    public function handle(): void
    {
        // the actual slow work: transcoding, generating thumbnails, etc.
        $this->video->generateThumbnails();
        $this->video->update(['status' => 'processed']);
    }
}

ShouldQueue is what marks this job as something to actually push onto a queue rather than run synchronously — a job class without this interface runs immediately, inline, when dispatched.

Dispatching the job

ProcessVideoUpload::dispatch($video);

This is essentially instant — it just serializes the job and pushes it onto the configured queue, returning control back to the request immediately, without waiting for handle() to actually run.

Configuring a queue driver

// .env
QUEUE_CONNECTION=database
// or, for production: redis, sqs, etc.

The database driver (storing queued jobs as rows in a jobs table) is a reasonable starting point requiring no extra infrastructure — Redis or Amazon SQS are more commonly used in production for better performance and reliability at real scale.

Running the queue worker

php artisan queue:work

This is the actual background process that picks up queued jobs and runs their handle() method — a queued job dispatched with no worker process running simply sits waiting in the queue indefinitely, never actually executing, which is a common early confusion when queues seem to silently "not work."

Keeping the worker running persistently with Supervisor

; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=2

A queue worker running via php artisan queue:work directly in a terminal stops the moment that terminal closes — Supervisor (or a similar process manager) keeps it running persistently in production, automatically restarting it if it crashes, and numprocs lets multiple worker processes run in parallel for more throughput.

Handling a failed job

public function failed(Throwable $exception): void
{
    Log::error('Video processing failed', ['video_id' => $this->video->id, 'error' => $exception->getMessage()]);
    $this->video->update(['status' => 'failed']);
}

--tries=3 (from the Supervisor config above) retries a failing job up to 3 times before giving up — the failed() method runs once retries are exhausted, giving a place to log the failure or notify someone rather than letting it disappear silently.

Dispatching after the current database transaction commits

ProcessVideoUpload::dispatch($video)->afterCommit();

Dispatching from inside a database transaction risks the queue worker picking up and running the job before that transaction actually commits — afterCommit() delays dispatch until the transaction is confirmed committed, avoiding the job trying to work with data that technically doesn't exist in the database yet from its perspective.