Have your users ever waited seconds after clicking "Submit", just because your app was sending emails or processing images behind the scenes? Heavy tasks like these should never make users wait. This is where Queues and Jobs come in: they move heavy work into the background so responses stay fast. This tutorial covers Queues in Laravel 10/11 from scratch with real code.
Why You Need Queues
By default, every request is processed synchronously — the user waits until all code finishes. If an action triggers sending 100 emails or compressing a 5 MB image, the request can take several seconds. With a queue, you simply hand off the work to the queue, return an instant response, and let a worker process it in the background.
- Faster responses: users don't wait for heavy tasks.
- Load resilience: spikes of work are processed gradually, not all at once.
- Automatic retries: failed jobs can be retried without disrupting the user.
1. Configure the Queue Driver
Laravel supports several drivers: sync (default, runs immediately), database, redis, and more. To start, the database driver is easiest since it needs no extra service. Set it in .env:
QUEUE_CONNECTION=database
Then create the jobs table and run the migration:
php artisan make:queue-table
php artisan migrate
For high traffic, redis is much faster. Just switch to QUEUE_CONNECTION=redis once a Redis server is available — your job code doesn't change at all.
2. Create Your First Job
Create a job class with artisan:
php artisan make:job SendWelcomeEmail
The file appears at app/Jobs/SendWelcomeEmail.php. Pass the data you need through the constructor, and put the heavy logic in the handle() method:
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user->email)
->send(new WelcomeMail($this->user));
}
}
The key here is the ShouldQueue interface — it tells Laravel this job should be queued rather than run immediately.
3. Dispatch the Job
From a controller, just call dispatch():
public function store(Request $request)
{
$user = User::create($request->validated());
SendWelcomeEmail::dispatch($user);
return redirect()->route('dashboard')
->with('success', 'Account created, email on its way.');
}
Notice the response returns instantly. The job goes into the queue, and email sending happens in the background. You can also delay a job with delay():
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
4. Run the Worker
A dispatched job only sits in the queue; something must execute it. Run a worker:
php artisan queue:work
The worker keeps running, picks up jobs one at a time, and runs the handle() method. For development, use queue:listen which reloads code automatically. For production, use queue:work because it is more efficient.
Important: after changing job code, restart the worker so changes are picked up:
php artisan queue:restart
5. Retries and Failed Jobs
Jobs can fail — an SMTP connection drops, a third-party API errors out. Laravel can retry automatically. Set the attempt limit via properties on the job:
public $tries = 3;
public $backoff = 10; // wait 10 seconds between attempts
Once all attempts are exhausted, the job lands in the failed_jobs table. Set up the table:
php artisan make:queue-failed-table
php artisan migrate
List, retry, or clear failed jobs:
php artisan queue:failed
php artisan queue:retry all
php artisan queue:flush
You can also handle failure specifically with a failed() method on the job, for example to notify an admin.
6. Real Example: Processing Images
Compression and thumbnail generation are heavy tasks. Wrap them in a job:
class ProcessProductImage implements ShouldQueue
{
use Dispatchable, Queueable, SerializesModels;
public function __construct(public Product $product) {}
public function handle(): void
{
$img = Image::read($this->product->image_path)
->scale(width: 800);
$img->save(storage_path('app/public/thumbnails/'
. $this->product->id . '.jpg'));
}
}
Dispatch it after the upload finishes, and the user never waits for image processing to complete.
7. Keeping Workers Alive with Supervisor
On a production server, the queue:work process can die at any time. Supervisor is a process monitor on Linux that automatically restarts the worker. Example config at /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
command=php /var/www/app/artisan queue:work --tries=3 --timeout=90
autostart=true
autorestart=true
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
Load the config:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
Now the worker always runs even if the server restarts. Your app handles heavy tasks without making users wait — the same pattern works for reports, WhatsApp notifications, Excel exports, and other batch processes.