Jobs & Queues
Some work a web request triggers is simply slow: sending an email, resizing an uploaded image, calling a third-party API that takes a few seconds to respond. If that work happens directly inside the request/response cycle, the user sits there staring at a spinner until it finishes — even though none of that work actually needs to happen before you can tell them "done." Laravel's queue system lets you push that slow work into the background: the user gets an instant response, and a separate process picks up the actual work moments later. This page covers what a job is, how to dispatch one, the available queue drivers, running a worker to process them, handling failures, and a full worked example.
The Problem Queues Solve
| Without a queue | With a queue |
|---|---|
| The request thread sends the email itself, and the user's browser waits for that to finish before the page loads | The request thread hands the email-sending work off and responds immediately; the email is sent moments later by a separate process |
| A slow third-party API call (e.g. resizing a video) can make a request take 10+ seconds or time out | The slow call happens outside the request entirely, with no risk of the user's request itself timing out |
A Job Represents One Unit of Background Work
A job is a plain PHP class representing a single piece of work that can be done independently of the request that triggered it. Generate one with Artisan:
$ php artisan make:job ProcessPodcastUpload
This creates app/Jobs/ProcessPodcastUpload.php. For the class to actually be
queueable (rather than just a plain class), it implements the ShouldQueue
interface, and its handle() method contains the real work:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcastUpload implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Podcast $podcast)
{
}
public function handle(): void
{
// the actual, potentially slow, work happens here
$this->podcast->transcode();
$this->podcast->update(['status' => 'ready']);
}
}
Dispatching a Job
Dispatching a job returns almost instantly — it doesn't run handle() right then
and there. Instead, it serializes the job and pushes it onto a queue for a worker to pick up
later:
<?php
ProcessPodcastUpload::dispatch($podcast);
return redirect()->route('podcasts.index')->with('status', 'Upload received, processing now.');
The redirect happens immediately; transcode() runs later, out of band, whenever
a queue worker gets to it.
Queue Drivers
The queue "driver" (or "connection") determines where queued jobs are actually
stored while they wait to be processed. This is configured in config/queue.php
and via the QUEUE_CONNECTION value in .env:
| Driver | What it does |
|---|---|
sync | Runs the job immediately, in the same request, instead of actually queueing it. Nothing is deferred — useful for local development and testing, where you don't want to run a separate worker process just to see a job's effects |
database | Stores queued jobs as rows in a database table (typically named jobs). Simple to set up since it needs no extra infrastructure beyond the database you already have |
redis | Stores queued jobs in Redis. Considerably faster than the database driver under real load, and the most common choice for production applications with meaningful traffic |
Running a Queue Worker
Dispatching a job only ever adds it to the queue — something still has to take jobs
off the queue and actually run their handle() method. That something is a
queue worker, started with:
$ php artisan queue:work
queue:work starts a long-running, persistent
process — it does not process one job and exit. In production this needs to be kept running by
a process monitor such as Supervisor, so that it automatically restarts if it crashes or the
server reboots. It is not something that "just happens" on its own; without a worker running,
jobs will sit in the queue table forever, never processed.Handling Job Failures
Jobs can fail — a third-party API might be down, a file might be missing. Laravel retries a failing job automatically (based on the job's configured retry attempts), and if it still fails after those attempts are exhausted, two things are available to you:
| Mechanism | Purpose |
|---|---|
A failed() method on the job class | Runs when the job has permanently failed, so you can react — for example notifying an administrator or cleaning up partial work |
The failed_jobs database table | Laravel automatically records permanently-failed jobs here, including their exception, so you can inspect and optionally retry them later with php artisan queue:retry |
public function failed(\Throwable $exception): void
{
// e.g. notify an admin that this podcast failed to process
Log::error("Podcast {$this->podcast->id} failed to process.");
}
Full Worked Example: Sending an Email in the Background
A common, concrete use case: after a contact form is submitted, an acknowledgement email needs to go out, but there's no reason to make the visitor wait for an SMTP round-trip before seeing "message sent." First, the job:
<?php
namespace App\Jobs;
use App\Mail\ContactAcknowledgement;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendContactAcknowledgement implements ShouldQueue
{
use Dispatchable, SerializesModels;
public function __construct(public string $email, public string $name)
{
}
public function handle(): void
{
Mail::to($this->email)->send(new ContactAcknowledgement($this->name));
}
}
Then, the controller handling the form submission simply dispatches it:
class ContactController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email',
]);
// ...save the contact message to the database here...
SendContactAcknowledgement::dispatch($validated['email'], $validated['name']);
return back()->with('status', 'Thanks — we\'ll be in touch!');
}
}
The visitor sees "Thanks — we'll be in touch!" immediately. The actual email goes out shortly
after, whenever php artisan queue:work picks the job up — completely invisible
to, and never blocking, the person who submitted the form.