Why Use Queues in Laravel 12?
If your Laravel application sends emails, processes file uploads, generates PDF reports, or calls external APIs during a web request, your users are waiting unnecessarily. Every second of delay increases bounce rates and hurts your SEO rankings. Laravel 12 queues solve this by pushing heavy tasks into the background — keeping your app fast and your users happy.Queues allow asynchronous processing. Instead of executing a slow task during an HTTP request, you push it to a queue. A separate process (the worker) handles it later.
Benefits include:
- Faster response times (better user experience and SEO)
- Scalability — handle traffic spikes without crashing
- Failure handling with automatic retries and exponential backoff
- Prioritization of critical tasks over less urgent ones
- Better server resource management

Table Of Content
1 Prerequisites
- PHP ≥ 8.2
- Composer installed globally
- MySQL / PostgreSQL / SQLite (or any Laravel-supported database)
- Basic knowledge of Laravel routing, controllers, and Blade templates
- A working Laravel 12 installation (or follow step 3 below to create one)
2 Introduction
If you're new to Laravel 12, make sure you have a solid understanding of routing and middleware before diving in. Also, familiarity with Eloquent ORM basics will help you follow along smoothly.
In this tutorial, you'll learn what queues and jobs are, how they work in Laravel 12, and how to implement them step-by-step with complete code examples. We'll build a real-world example: sending a welcome email asynchronously after user registration.
3 Create a Fresh Laravel 12 Project
3.1 Install Laravel Project
composer create-project laravel/laravel:^12.0 laravel-queue-demo
Navigate to your project directory:
cd laravel-queue-demo
3.2 Configure Your Database & Queue Driver in (.env)
QUEUE_CONNECTION=database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_queue
DB_USERNAME=root
DB_PASSWORD=
Queue driver options explained:- database — Easy setup, great for getting started (stores jobs in a DB table)
- redis — High performance, recommended for production (pair with Laravel Horizon for monitoring)
- sqs — AWS Simple Queue Service, ideal for cloud-native auto-scaling
- sync — Runs jobs immediately (useful for local debugging, not for production)
4 Generate & run Migrations for queues + failed jobs
php artisan queue:table
php artisan migrate
This creates a jobs table in your database to store queued tasks. Each row represents a pending job with its payload, attempts count, and queue name.For failed jobs tracking (highly recommended for production), also run:
php artisan queue:failed-table
php artisan migrate
This creates a failed_jobs table where Laravel stores jobs that exceeded their maximum retry attempts, along with the exception details for debugging. 5 Create the Mailable (welcome email)
php artisan make:mail WelcomeEmail --markdown=emails.welcome
Edit app/Mail/WelcomeEmail.php
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable
{
use Queueable, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to Our App, ' . $this->user->name . '!',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.welcome',
);
}
public function attachments(): array
{
return [];
}
}
?>
resources/views/emails/welcome.blade.php (simple markdown email)
@component('mail::message')
# Welcome, {{ $user->name }}!
Thank you for joining our amazing community.
We're excited to have you on board.
Happy exploring!
Thanks,<br>
{{ config('app.name') }} Team
@endcomponent
6 Create the Job
Generate a job class:
php artisan make:job SendWelcomeEmail
Edit app/Jobs/SendWelcomeEmail.php
<?php
namespace App\Jobs;
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;
use App\Mail\WelcomeEmail; // Assume you have this Mailable
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $user;
public $tries = 3; // Retry up to 3 times
public $backoff = [10, 30, 60]; // Wait 10s → 30s → 60s before retries
public function __construct(User $user)
{
$this->user = $user;
}
public function handle(): void
{
Mail::to($this->user->email)->send(new WelcomeEmail($this->user));
}
}
?>
Key things to note:- The ShouldQueue interface tells Laravel to push this job to the queue instead of running it synchronously.
- $tries = 3 means Laravel will attempt this job up to 3 times before marking it as failed.
- $backoff = [10, 30, 60] uses exponential backoff — waiting longer between each retry to avoid overwhelming external services.
- SerializesModels ensures the User model is properly serialized/unserialized when stored in the queue.
7 Dispatch the Job (Update Registration Controller)
Edit app/Http/Controllers/Auth/RegisteredUserController.php
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
public function create(): View
{
return view('auth.register');
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
// This is the key line — dispatch to queue!
SendWelcomeEmail::dispatch($user);
// Optional: delay it by 30 seconds
// SendWelcomeEmail::dispatch($user)->delay(now()->addSeconds(30));
Auth::login($user);
return redirect(route('dashboard'));
}
}
?>
8 Run the Queue Worker
Development
php artisan queue:work --queue=default --tries=3 --sleep=3
Common flags: - --queue=high,low — process high priority first
- --once — process one job then exit
- --stop-when-empty — useful in serverless
- --timeout=60 — kill if job runs >60s
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/project/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/path/to/your/project/storage/logs/worker.log
stopwaitsecs=3600
Then :
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
9 Handling Failed Jobs
View failed jobs:
php artisan queue:failed
Retry all failed jobs:
php artisan queue:retry all
Retry a specific job by its UUID: php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5eceFlush all failed jobs: php artisan queue:flush
Pro tip: You can also define a failed() method inside your job class to run custom logic (like sending an alert or logging to Slack) whenever that specific job fails. This is extremely useful for debugging common Laravel errors in production.
10 Run & Test the Application
- Run php artisan serve to start the development server
- Open a separate terminal and run: php artisan queue:work
- Register a new user at /register
- Check your mail trap (Mailpit, Mailhog, or set MAIL_MAILER=log in .env to log emails to storage/logs/laravel.log)
- The welcome email should arrive after the registration redirect — proving it was processed asynchronously
If something goes wrong, check our Common Laravel 12 Errors and How to Fix Them guide for troubleshooting.
Testing with Fake Queues
In your PHPUnit or Pest tests, you can fake the queue to assert jobs were dispatched without actually running them:
use Illuminate\Support\Facades\Queue;
Queue::fake();
$user = User::factory()->create();
SendWelcomeEmail::dispatch($user);
Queue::assertPushed(SendWelcomeEmail::class, function ($job) use ($user) {
return $job->user->id === $user->id;
});
11 Related Laravel 12 Tutorials
- Laravel 12 Performance Optimization Tips — Queues are just one piece of the performance puzzle. Learn 12 proven techniques including caching, eager loading, and database indexing to make your app lightning fast.
- Common Laravel 12 Errors and How to Fix Them — Running into issues with your queue setup? This guide covers the most frequent Laravel 12 errors and their solutions.
- How to Build a REST API in Laravel 12 — Combine background queue processing with REST API endpoints for scalable, async architectures.
- Laravel 12 Routing and Middleware Guide — Understand the request lifecycle that triggers your queued jobs, from routes to middleware to controllers.
- Laravel 12 Authentication with WorkOS AuthKit — Secure your user registration flow before dispatching welcome email jobs.
- Docker Compose for Laravel & MySQL — Deploy your queue workers inside Docker containers for consistent, reproducible environments.
- Laravel 12 Eloquent Tips and Tricks — Master the ORM that powers your job classes and model serialization.
12 Conclusion
In this tutorial, we covered the full queue workflow: configuring the database driver, creating a Mailable and a Job class, dispatching jobs from a controller, running queue workers, handling failures with retries and backoff, and testing with fake queues.
Start small — try the welcome email example above — then expand to image processing, report generation, or third-party integrations like WhatsApp API messaging.
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
A queue in Laravel 12 is a mechanism to defer time-consuming tasks (like sending emails, processing files, or calling APIs) so they run in the background instead of during the HTTP web request. This improves page load speed, user experience, and overall application performance.
Set QUEUE_CONNECTION=database (or redis/sqs) in your .env file. Then run php artisan queue:table followed by php artisan migrate to create the jobs table. Also run php artisan queue:failed-table and migrate for failed job tracking.
Run php artisan make:job YourJobName to generate the class. Then implement the handle() method with your logic and ensure the class implements the ShouldQueue interface. Set properties like $tries and $backoff for retry behavior.
Use php artisan queue:work combined with Supervisor to keep workers running and auto-restart on failure. For Redis queues, Laravel Horizon provides a dashboard and monitoring. For cloud deployments, consider Laravel Cloud or AWS SQS for auto-scaling.
After exceeding the maximum retry attempts ($tries), the job is moved to the failed_jobs table with its error details. You can retry failed jobs with php artisan queue:retry all (or a specific UUID), inspect them with php artisan queue:failed, or flush all with php artisan queue:flush.
Yes — use the delay() method when dispatching: SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10)). This tells Laravel to wait the specified duration before processing the job.
Use the database driver for quick setup and small projects. Use Redis for better performance, pub/sub support, and the Laravel Horizon monitoring dashboard. Use Amazon SQS for cloud-native applications that need auto-scaling and high availability.
