Laravel Reference
Why queue anything
Some work is too slow to do inline — sending an email, resizing an image, calling a third-party API — and making a request wait on it is a bad trade when the user only needs an acknowledgement, not the result. A queue lets that work happen after the response has already gone back, on a separate process. It's the same instinct behind task scheduling, just answering a different question: scheduling is about when something should run, queues are about how work actually runs off-request once it's been triggered.
Choosing a queue connection
The active driver is set via QUEUE_CONNECTION in .env:
QUEUE_CONNECTION=sync
| Driver | Behaviour |
|---|---|
sync | Runs the job immediately, inline, in the current process — there is no actual queueing. It's the default in a fresh install and is why "my queued job isn't queueing" is usually just this setting during local development. |
database | Stores jobs as rows in a table. No extra infrastructure needed — a solid default once real queueing is wanted. |
redis | Backed by Redis lists. Faster and more feature-complete (delayed jobs, atomic locks) than the database driver, and the usual choice in production. |
Switching to the database driver needs a table to store jobs in:
php artisan queue:table
php artisan migrate
Defining a job
php artisan make:job ProcessPodcast
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Podcast $podcast,
) {}
public function handle(AudioProcessor $processor): void
{
$processor->process($this->podcast);
}
}
Implementing ShouldQueue is what makes a job queueable at all — without it, dispatching just runs handle() synchronously. Like a controller method or an Artisan command's handle(), dependencies are type-hinted and resolved from the container automatically, on top of whatever was passed into the constructor and serialised onto the job.
Dispatching
ProcessPodcast::dispatch($podcast);
// Run at least ten minutes from now
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));
// Send to a specific queue name, for prioritisation
ProcessPodcast::dispatch($podcast)->onQueue('media');
Running a worker
php artisan queue:work
php artisan queue:listen
Both process jobs as they arrive, but they differ in one important way: queue:work boots the framework once and keeps reusing that same process for every job, which is far more efficient and is the right choice in production. queue:listen reboots the framework on every job, which is slower but means code changes are picked up immediately without restarting the worker — handy while iterating locally, since a long-running queue:work process otherwise keeps running the old code in memory until it's restarted. Like everything under php artisan, both are themselves just Artisan commands, and a custom command can dispatch jobs the same way a controller does.
Retries and backoff
class ProcessPodcast implements ShouldQueue
{
public $tries = 3;
public $backoff = 30; // seconds between attempts
// or vary the delay per attempt
public function backoff(): array
{
return [10, 30, 60];
}
public function failed(?Throwable $exception): void
{
// Notify someone, log extra context, etc.
}
}
When a job exhausts its attempts, it's moved to the failed_jobs table (created by the same queue:table/migrate step, or its own queue:failed-table migration) and its failed() method runs, if defined. From there:
php artisan queue:failed # list failed jobs
php artisan queue:retry 5 # retry one, by id
php artisan queue:retry all # retry everything
php artisan queue:forget 5 # delete one failed job
Batching and chaining
A chain runs a series of jobs one after another, stopping if any of them fails:
Bus::chain([
new ProcessPodcast($podcast),
new OptimisePodcast($podcast),
new NotifySubscribers($podcast),
])->dispatch();
A batch runs a group of jobs with no ordering guarantee between them, but tracks overall progress and completion as a set:
$batch = Bus::batch([
new ProcessPodcast($podcastOne),
new ProcessPodcast($podcastTwo),
])->then(function (Batch $batch) {
// All jobs completed successfully
})->catch(function (Batch $batch, Throwable $e) {
// First failure
})->dispatch();
Keeping a worker running in production
php artisan queue:work is a long-running process, not a request — it needs something outside of PHP to keep it alive, restart it if it crashes, and start it again after a server reboot. A process supervisor like systemd or Supervisor is the usual tool for that job, running alongside whatever's serving the app itself; see the EC2 and Lightsail pages for the surrounding server setup. Also worth remembering: a running worker holds the app's code in memory the same way Tinker does, so a deploy needs to restart workers (php artisan queue:restart signals them to finish their current job and exit, ready for the supervisor to bring them back up on the new code) or old code keeps running until it does.
Queued mail
Mailables commonly implement ShouldQueue too, for exactly the same reason as any other slow work — see the mail and notifications guide.