A Laravel queue lets you move slow work — sending a welcome email, processing an image, calling an external API — out of the request-response cycle and into the background. The user no longer waits for the email to actually go out; the task is pushed onto a queue and handled later by a background worker when its turn comes. In this guide we will set up the queue infrastructure from scratch, write our own job class, run the worker, and handle failures robustly.
Why you need a queue
In a synchronous application everything happens inside the same request. The user submits the registration form, you send the email, maybe resize an image, and only then does the response return to the browser. If the mail server is slow, the user waits for seconds. A queue separates these tasks:
- Speed: Slow work goes to the background and the response returns instantly.
- Resilience: If a job fails, it can be retried automatically.
- Scalability: You can run multiple workers and spread the load.
Choosing a queue driver
Laravel supports several queue drivers, all sitting behind a single API. You pick one with the QUEUE_CONNECTION variable in your .env file:
QUEUE_CONNECTION=database
The common options are:
sync— the default; the job runs immediately in the same request. Useless outside local development.database— stores jobs in a database table. Easy to set up, plenty for small/medium projects.redis— the fast, recommended choice for high volume.sqs— a managed, serverless queue via Amazon SQS.
If you use the database driver, you need a table for the queue. Laravel ships a command to generate it:
php artisan make:queue-table
php artisan migrate
Creating your first job class
A job is a single unit of work to run on the queue. Create one with artisan:
php artisan make:job SendWelcomeEmail
This generates app/Jobs/SendWelcomeEmail.php. A class that implements the ShouldQueue interface is queued automatically. The handle() method holds the actual logic of the work:
<?php
namespace App\Jobs;
use App\Models\User;
use App\Mail\WelcomeMail;
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 function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
}
Note one thing: we pass a User model to the constructor. Thanks to the SerializesModels trait, only the primary key is serialized rather than the whole model; when the job runs, the model is freshly reloaded from the database.
Dispatching the job onto the queue
To trigger the job you use dispatch. The simplest form is the static method:
use App\Jobs\SendWelcomeEmail;
SendWelcomeEmail::dispatch($user);
With fluent chained methods you can tune how the job runs:
SendWelcomeEmail::dispatch($user)
->onQueue('emails') // to a specific queue
->delay(now()->addMinutes(5)); // delayed by 5 minutes
To run several jobs in sequence use Bus::chain(), and for parallel groups use Bus::batch(). It is fine to start small; you add these features as you need them.
Running the worker
Jobs land on the queue, but you need a process to handle them. That is what the worker is for:
php artisan queue:work
This command runs continuously and waits for new jobs. A few important flags:
--queue=emails,default— listens to queues in priority order.--tries=3— how many times a job is attempted if it fails.--timeout=60— how many seconds a single job may run at most.
A crucial point: queue:work loads your application into memory. When you change the code, the worker keeps running the old version. That is why you must restart the worker after each deploy:
php artisan queue:restart
In production, use a process manager like Supervisor so the worker comes back up even if it crashes. Supervisor monitors the queue:work process and restarts it automatically when needed.
Handling failed jobs
Jobs sometimes fail: the mail server does not respond, an API times out. When all attempts are exhausted, Laravel records the job in the failed_jobs table. Create the migration for it:
php artisan make:queue-failed-table
php artisan migrate
You can list failed jobs and retry them:
php artisan queue:failed # list
php artisan queue:retry all # retry all
php artisan queue:flush # delete all
If you add a failed() method to your job class, it runs when the job fails permanently; there you can notify an admin or write a log. You can also use the $backoff property to add a wait between retries — this is critical so you do not overwhelm a flaky service during temporary outages.
Frequently Asked Questions
How do I choose between the database and redis drivers?
Up to a few thousand jobs a day, database is perfectly fine and needs no extra infrastructure. Move to Redis if you need high volume, low latency, or advanced features like Bus::batch(). Most projects start with database and switch once the need arises.
Why do I have to restart the worker after a code change?
Because queue:work is a long-lived process that loads the application into memory once. Code you change is not reflected in the in-memory copy. php artisan queue:restart lets the current jobs finish and tells the worker to restart safely.
How long can a single job run?
It is set with the --timeout flag (60 seconds by default). For longer jobs increase the value, but make sure the timeout is shorter than the queue's retry_after setting; otherwise the same job could be processed twice.
If you want your queue up and running, we can find the slow spots in your project together and push them to the background; get in touch with me to set up or optimize your Laravel infrastructure.