Laravel Queues and Jobs
Dispatching a queued job, a complete Job class example, and why queues matter for slow work.
Why queues matter
Some work a request triggers doesn't need to finish before the response goes back to the user — sending a confirmation email, generating a PDF report, calling a slow third-party API, resizing an uploaded image. Doing that work synchronously, inline in the request/response cycle, means the user's browser sits waiting for however long that slow operation takes — a genuinely bad experience, and one that gets worse under load since a slow external service (an email provider having a bad day) ties up a web server worker for every affected request.
A queue decouples "do this work" from "do it right now": the request pushes a small, serializable job onto a queue and returns immediately, and a separate worker process picks that job up and runs it in the background, independent of any waiting browser.
Configuring a queue driver
# .env
QUEUE_CONNECTION=database
php artisan queue:table
php artisan migrate
database is the simplest real driver — jobs are stored as rows in a jobs table, no extra infrastructure needed beyond the database Laravel is already using. Production deployments under real load more commonly reach for Redis (QUEUE_CONNECTION=redis) instead, since it doesn't compete with the application's own database for write throughput — but the job-writing code below is identical either way; only .env changes.
A complete Job class
php artisan make:job SendWelcomeEmail
<?php
// app/Jobs/SendWelcomeEmail.php
namespace App\Jobs;
use App\Mail\WelcomeEmail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30; // seconds to wait before retrying after a failure
public function __construct(
public User $user,
) {}
public function handle(): void
{
Mail::to($this->user->email)->send(new WelcomeEmail($this->user));
}
public function failed(\Throwable $exception): void
{
// runs after all retry attempts are exhausted — log it, alert someone, etc.
report($exception);
}
}
ShouldQueue is what actually makes this job asynchronous — implementing it tells Laravel to serialize the job and push it onto the queue instead of running handle() immediately. SerializesModels stores just the User's primary key in the queued payload, then automatically re-fetches a fresh instance from the database when the job actually runs — important, since a job might not execute until minutes after it was dispatched, by which point an eagerly-serialized User object would hold stale data. $tries and $backoff control retry behavior for a job that throws an exception; failed() is the one place to handle "this job could not be completed after every retry."
Dispatching the job
<?php
// app/Http/Controllers/RegisterController.php
namespace App\Http\Controllers;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Http\Request;
class RegisterController extends Controller
{
public function store(Request $request)
{
$user = User::create($request->validated());
SendWelcomeEmail::dispatch($user);
return redirect()->route('dashboard');
}
}
SendWelcomeEmail::dispatch($user) pushes the job onto the configured queue and returns immediately — the controller redirects the newly registered user to their dashboard without ever waiting for the welcome email to actually send. Delaying a job is just as direct:
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
Running the queue worker
None of the above does anything unless a worker process is actually running to pick jobs off the queue:
php artisan queue:work
queue:work runs continuously, pulling and processing jobs one at a time (or with multiple worker processes for more throughput) until stopped — in production, it's kept running under a process monitor like Supervisor, which restarts it automatically if it crashes or if php artisan queue:restart is issued to pick up deployed code changes gracefully.
Common mistakes
- Forgetting to run
php artisan queue:work(or a Supervisor-managed equivalent) at all — dispatched jobs pile up in thejobstable (or Redis) and simply never execute until a worker is actually running. - Passing a large, non-serializable object (like an open file handle) into a job's constructor instead of relying on
SerializesModelsfor Eloquent models — it either fails to serialize at all or bloats the queue payload unnecessarily. - Not deploying code changes with
php artisan queue:restart— a long-running worker process keeps the old code loaded in memory until told to restart, so a deployed bug fix silently doesn't apply to jobs already picked up by a stale worker.
Interview questions
Q: Why dispatch a job to a queue instead of just running the same code synchronously in the controller?
Any slow operation — sending an email, calling a third-party API, generating a report — ties up a web server worker and makes the user wait for its full duration if run synchronously. Dispatching it as a queued job lets the controller return a response immediately while a separate worker process handles the actual work in the background, and it adds retry behavior ($tries, $backoff) for operations that can transiently fail, like an email provider being briefly unavailable.
Q: Why does a Job class use SerializesModels instead of just holding the model instance directly in its constructor?
A queued job's constructor arguments are serialized and stored (in the jobs table, or Redis) until a worker actually picks it up — which might be seconds or, after a delay, minutes later. SerializesModels stores only the model's primary key in that serialized payload and re-fetches a fresh instance from the database at execution time, so the job always operates on current data rather than a stale snapshot captured back when it was first dispatched.