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.