A queue lets your app say “I'll do this later” by saving a job to a list, while a separate worker process picks jobs off that list and runs them in the background.
🍔At a burger counter, the cashier takes your order, gives you a ticket and moves on to the next customer. The kitchen (the worker) cooks the orders in turn. Customers aren't kept standing at the till while each burger is made.
Users shouldn't wait for slow work
Snappy responses
Sending an email can take 1–3 seconds. Queue it, and the page responds in milliseconds.
Automatic retries
If the mail server hiccups, the job is retried later instead of showing the user an error.
Heavy lifting, safely
Resizing images, generating PDFs and importing CSVs can run for minutes without hitting web request timeouts.
From request to background job
queue:work runs the job- Job
- A small class describing one piece of work, with a
handle()method. - Queue
- The waiting list of jobs, stored by a driver (database, Redis, SQS…).
- Worker
- A long-running command (
php artisan queue:work) that takes jobs from the queue and runs them. - Dispatch
- Putting a job onto the queue.
Your first queued job
-
Configure the database queue driver
New Laravel apps already use the
databasedriver, and thejobsandfailed_jobstables are created by the default migrations. Check your.env:QUEUE_CONNECTION=databaseIf the tables are missing in an older app, run
php artisan make:queue-tablethenphp artisan migrate. Thesyncdriver runs jobs immediately (no queue), which is handy in tests. In production at scale,redisis the usual choice. -
Create a job with
make:jobphp artisan make:job NotifyFollowersThis creates
app/Jobs/NotifyFollowers.php. TheShouldQueueinterface is what tells Laravel to queue it rather than run it immediately.<?php namespace App\Jobs; use App\Models\Post; use App\Notifications\NewPostPublished; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; class NotifyFollowers implements ShouldQueue { use Queueable; public function __construct(public Post $post) {} public function handle(): void { foreach ($this->post->author->followers as $follower) { $follower->notify(new NewPostPublished($this->post)); } } }Passing a model is safe: Laravel stores only its ID in the queue and re-fetches the fresh model when the job runs.
-
Dispatch it
public function publish(Post $post) { $post->update(['published_at' => now()]); NotifyFollowers::dispatch($post); return back()->with('status', 'Published!'); }A few useful variations:
NotifyFollowers::dispatch($post)->delay(now()->addMinutes(5)); NotifyFollowers::dispatch($post)->onQueue('emails'); NotifyFollowers::dispatchAfterResponse($post); // tiny tasks, no worker neededMailables and notifications can be queued too: add
implements ShouldQueueto the class, or useMail::to($user)->queue(...). -
Run a worker with
queue:workNothing happens to queued jobs until a worker is running. In development, open a second terminal:
php artisan queue:work php artisan queue:work --queue=emails,default # priority order⚠️ Workers keep old code in memoryqueue:workboots your app once and keeps running, so code changes aren't picked up. Restart it after edits (or usequeue:listenlocally). On deploy, runphp artisan queue:restart, and keep workers alive with a process manager like Supervisor. -
Handle retries and failed jobs
If
handle()throws an exception, the job goes back on the queue and is tried again. Control how many times and how long to wait between attempts:public int $tries = 3; public function backoff(): array { return [10, 60, 300]; // wait 10s, then 60s, then 5 min } public function failed(?Throwable $e): void { // runs after the final attempt fails: alert someone, log it }After the last attempt, the job is stored in the
failed_jobstable. Inspect and retry from the command line:php artisan queue:failed # list failed jobs php artisan queue:retry all # push them back onto the queue php artisan queue:retry 5 # retry one by ID php artisan queue:flush # delete all failed jobs
When you move to Redis, Laravel Horizon (composer require laravel/horizon) gives you a dashboard of throughput, runtimes and failures, plus code-based worker configuration. It replaces running queue:work by hand.
Avoid this, do that
foreach ($followers as $f) {
Mail::to($f)->send(new NewPost($post));
}
return back();
200 followers × 1 second each: the user stares at a spinner for minutes, or the request times out.
NotifyFollowers::dispatch($post);
return back();
The response is instant, and the worker sends emails in the background with retries.
public function __construct(
public UploadedFile $file
) {}
Jobs are serialised into storage. Uploaded files, closures and open connections don't survive the trip.
$path = $request->file('csv')->store('imports');
ImportPosts::dispatch($path);
Store the file first and pass simple data (a path, an ID, a model).
Retries mean a job might run more than once. Design handle() so a second run doesn't double-charge or double-send, for example by checking “already notified?” before acting.
- Create a
LogPostPublishedjob that writes to the log withLog::info(), then dispatch it from a route. - Visit the route without a worker running and look at the
jobstable. - Start
php artisan queue:workand watch the job get processed. - Make
handle()throw an exception, set$tries = 2, and find it withphp artisan queue:failed. - Fix the bug and run
php artisan queue:retry all.
Key takeaways
- Create jobs with
php artisan make:job;implements ShouldQueuemakes them run in the background. - Send them with
MyJob::dispatch(...), optionally withdelay()oronQueue(). QUEUE_CONNECTION=databaseworks out of the box; Redis (with Horizon) is the production upgrade.- Nothing runs without
php artisan queue:work; restart workers after deploying. - Use
$tries,backoff()andfailed(); manage failures withqueue:failedandqueue:retry.