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 queueWith a queue
The request thread sends the email itself, and the user's browser waits for that to finish before the page loadsThe 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 outThe 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:

DriverWhat it does
syncRuns 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
databaseStores 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
redisStores 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
Important: 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:

MechanismPurpose
A failed() method on the job classRuns when the job has permanently failed, so you can react — for example notifying an administrator or cleaning up partial work
The failed_jobs database tableLaravel 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.

Next up: Continue to Task Scheduling to see how Laravel automates running jobs and commands on a recurring schedule.