Hello! Welcome back to our series on mastering Laravel.
In our last lesson, we built a solid foundation for creating resilient background tasks by learning how to handle and retry failed jobs. You now understand how Laravel can automatically retry jobs, how to inspect failures, and how to perform cleanup actions.
Today, we will put that knowledge to practical use by addressing one of the most common reasons for using queues in the first place. This lesson focuses on the learning outcome: Offload long-running tasks like sending emails or notifications to a queue. We'll explore why this is critical for a fast, modern web application and walk through the exact steps to move a slow, synchronous process into an asynchronous background job.
1. The Problem: The Waiting Game
Every time a user interacts with your application—like submitting a form—they trigger a request-response cycle. The user's browser sends a request to your server, your Laravel application processes it, and then sends a response back. The user has to wait for this entire process to complete.
Now, imagine a user registers for your application. Your code might look like this:
- Validate the user's input.
- Create a new
Userrecord in the database. - Send a welcome email.
- Redirect the user to their dashboard.
Steps 1, 2, and 4 are usually very fast. But step 3, sending an email, can be slow. It involves communicating with an external mail server (like Mailgun or an SMTP server), which could take several seconds. During this time, the user is staring at a loading screen, waiting. This creates a poor user experience.
The solution is to perform this slow task asynchronously—that is, in the background, outside of the user's request.
To see the dramatic difference this makes, let's watch two quick clips.
30 Days to Learn Laravel, Ep 25 - Queues Are Easier Than You Think
First, this clip from Laracasts clearly illustrates why making the user wait for an email to send is a bad idea and shows the immediate improvement when the task is queued.
Watch the first 51 seconds of the video. The presenter explains the problem of synchronous email sending and introduces the concept of throwing the job into the background.
Queues in Laravel: Main Things You Need to Know (Two Examples)
Next, this video from Laravel Daily provides a direct, timed comparison between a synchronous and an asynchronous notification. It also shows a modern, powerful way to make notifications queuable.
Watch from 01:05 to 02:28. Notice the stark difference in response time in the browser's network tab, and pay attention to how a Laravel Notification class is made queuable by simply implementing an interface.
As you can see, the performance gain and improvement in user experience are immediate and significant. Now, let's learn how to implement this ourselves.
2. The Solution: Building an Asynchronous Job
While Laravel provides shortcuts like Mail::queue() or queuable notifications, the most flexible and common approach for handling background tasks is to create a dedicated Job Class. This class encapsulates the logic for a single task you want to run in the background.
Let's refactor our user registration process to send the welcome email asynchronously.
Step 1: Create the Job Class
First, we'll use an Artisan command to generate a new job class.
php artisan make:job SendWelcomeEmail
This command creates a new file at app/Jobs/SendWelcomeEmail.php. Let's open it and set it up.
Laravel Queues & Jobs: Complete Guide with Redis & Horizon
This blog post provides an excellent, complete example of a SendWelcomeEmail job. We'll use its structure as our blueprint.
Review the code block in the section 'Creating Your First Job'. Don't worry about the $tries or failed() method for now (we covered those last lesson). Focus on the ShouldQueue interface, the __construct() method, and the handle() method.
Based on that example, our job class will look like this:
<?php
namespace App\Jobs;
use App\Models\User;
use App\Mail\WelcomeMail; // Assuming you have a Mailable class
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;
/**
* Create a new job instance.
*/
public function __construct(
public User $user
) {}
/**
* Execute the job.
*/
public function handle(): void
{
Mail::to($this->user->email)->send(new WelcomeMail($this->user));
}
}
Let's break down the key parts:
implements ShouldQueue: This interface is the magic ingredient. It tells Laravel that this job should be pushed onto a queue instead of being run immediately.__construct(public User $user): We use the constructor to pass in the data the job needs to do its work. Here, we need theUsermodel to know whom to email.handle(): void: This is where the actual logic goes. When the queue worker picks up this job, it will execute the code inside this method.

A Note on Model Serialization
You might wonder if passing an entire User object into the job is efficient. Laravel is very smart about this.
Queues - Laravel Documentation
The official documentation explains this behavior perfectly. Understanding it is key to writing efficient jobs.
Read the section 'Class Structure'. Focus on the paragraph that begins: 'If your queued job accepts an Eloquent model in its constructor...'.
As the docs explain, when your job is queued, Laravel doesn't serialize the entire model object. It only serializes the model's identifier (e.g., the primary key). When the worker processes the job, it re-retrieves the fresh model from the database. This keeps your queue payloads small and prevents issues with stale data.
Step 2: Dispatch the Job
Now that our job class is ready, we just need to "dispatch" it from our controller. We'll replace the synchronous Mail::send() call with a call to our new job's dispatch() method.
In your RegisteredUserController (or wherever your user registration logic lives), find the spot after the user is created and make this change:
// Before:
// Mail::to($user->email)->send(new WelcomeMail($user));
// After:
SendWelcomeEmail::dispatch($user);
That's it! Your application will now create the user, immediately dispatch the SendWelcomeEmail job to the queue, and redirect the user to their dashboard without waiting. A queue worker you have running in the background (php artisan queue:work) will pick up the job and send the email.
3. Best Practices for Offloading Tasks
Now that you know the mechanics, let's cover a few best practices to make your queue system more robust and organized.
Use Dedicated Queues
As your application grows, you'll have different kinds of jobs: some urgent (like password resets), some normal (like welcome emails), and some slow (like generating a monthly report). If they all go into the same default queue, a long-running report job could hold up an urgent password reset.
The solution is to categorize your jobs using named queues. You can specify the queue a job should go to using the onQueue() method when dispatching.
// In a password reset controller
SendPasswordResetEmail::dispatch($user)->onQueue('high-priority');
// In our registration controller
SendWelcomeEmail::dispatch($user)->onQueue('emails');
// For a slow, non-urgent task
GenerateMonthlyReport::dispatch()->onQueue('low-priority');
You can then run workers that prioritize certain queues:
```grasp
{
"type": "exercise",
"id": "4d914ef1-c9d7-4f5d-9f04-855541fe8a7f"
}
This worker will process jobs from 'high-priority' first, then 'emails', then 'low-priority'
php artisan queue:work --queue=high-priority,emails,low-priority
This strategy ensures that urgent tasks are always handled promptly.
#### Delaying a Job
Sometimes you don't want a job to run immediately. A common pattern is to send a follow-up email or notification a day after a user signs up. You can achieve this with the `delay()` method.
```php
// Send a follow-up email 24 hours after registration
SendFollowUpEmail::dispatch($user)->delay(now()->addDay());
The job will be pushed to the queue but will not be available for a worker to process until the specified time has passed.
Reviewing Dispatch Options
The official documentation provides a great summary of all the ways you can dispatch jobs.
Queues - Laravel Documentation
It's worth reviewing the different ways you can dispatch a job. The dispatchIf and delay methods are particularly useful for real-world applications.
Read the sections 'Dispatching Jobs' and 'Delayed Dispatching' to see the full range of available methods.
Conclusion
You have now mastered a fundamental and high-impact performance optimization technique in Laravel. By offloading time-consuming tasks like sending emails and notifications to a background queue, you make your application feel significantly faster and more responsive to the user.
Key Takeaways:
- Synchronous long-running tasks (like API calls, email sending) in a web request block the user and create a poor experience.
- Offload these tasks to background jobs by creating a Job class that implements the
ShouldQueueinterface. - Pass required data to the job's constructor. Laravel efficiently handles Eloquent models by serializing only their IDs.
- Place the core logic of the task inside the job's
handle()method. - Replace your synchronous code with a call to
YourJob::dispatch(). - Use named queues (
onQueue()) to categorize and prioritize jobs for better throughput. - Use
delay()to schedule jobs to run in the future.
Up Next:
We've successfully offloaded a relatively simple, self-contained task. But what happens when you need to process a very large task that needs to be broken into smaller pieces, like importing a huge CSV file with thousands of rows? In our next lesson, we will tackle exactly that, learning how to process large file uploads and data imports using queued jobs, which will introduce us to another powerful feature: Job Batching.