Imagine you run a restaurant app. A customer signs up, and your app needs to send a welcome email, generate a PDF receipt, and update a third-party CRM — all at the same time. If you do all of that during the web request, the user stares at a loading spinner for 3–5 seconds. That is a terrible experience.
Laravel queues solve this problem elegantly. Instead of doing heavy work immediately, you push it onto a queue — a waiting list — and a background worker handles it independently. The user's response comes back in milliseconds, and the heavy work happens silently behind the scenes.
By the end of this guide you will be able to set up queues, create jobs, dispatch them, run workers, handle failures, add delays, and configure priorities — all from scratch, using Laravel's database driver (no Redis required to start).
You need a working Laravel 10/11/12 project, PHP 8.1+, and Composer installed. Basic knowledge of Laravel controllers and Eloquent models is assumed.
1 What Are Queues and Jobs? (The Mental Model)
Before writing any code, let's lock in the key concepts. There are three things you need to understand:
- Job — A PHP class that holds the task you want to run in the background (e.g., SendWelcomeEmail, GenerateInvoicePDF, ResizeProductImage).
- Queue — The channel (a database table, Redis list, or SQS queue) where dispatched jobs wait to be picked up.
- Worker — A long-running process (php artisan queue:work) that continuously picks jobs off the queue and executes them.
Here is how the full flow looks in a typical Laravel application:
The user gets an instant response. The heavy work happens asynchronously in the background.
The controller dispatches the job and immediately returns a response to the user. The worker picks up the job and runs it whenever it is ready — completely independently of the web request.
2 Choosing a Queue Driver
Laravel supports several queue backends. For beginners, start with database — it requires zero extra software. As your application scales, switch to Redis.
| Driver | Best For | Pros | Cons | Badge |
|---|---|---|---|---|
| database | Small projects, learning | No extra setup, built-in | Slower under high load | Beginner |
| redis | Medium–large production apps | Very fast, supports Horizon | Requires Redis server | Production |
| sqs | AWS infrastructure | Fully managed, auto-scales | Requires AWS account & config | Enterprise |
| sync | Local testing only | Runs jobs immediately (no worker) | Not asynchronous — defeats the purpose | — |
This guide uses the database driver throughout. You can switch to Redis later by changing one line in your .env file.
3 Step 1 — Configure the Queue Driver
Open your .env file and set the queue connection to database:
# Change this line in your .env file QUEUE_CONNECTION=database # For Redis instead (later), use: # QUEUE_CONNECTION=redis
Next, create the jobs table in your database. In Laravel 12, this migration is already included by default. If it's missing, generate and run it:
# Only needed if the jobs migration doesn't exist yet php artisan queue:table # Run all pending migrations php artisan migrate
This creates a jobs table in your database where queued jobs will be stored until a worker picks them up. Open your database client — you should now see the jobs table with columns like id, queue, payload, attempts, and reserved_at.
That's it for configuration. No Redis, no SQS, no extra dependencies. The database driver works right out of the box. Now let's create your first job.
4 Step 2 — Create Your First Job
Use Artisan to generate a job class. We'll create a SendWelcomeEmail job as a practical example:
php artisan make:job SendWelcomeEmailThis creates app/Jobs/SendWelcomeEmail.php. Open it and you'll see a class with two key methods: __construct() and handle(). Let's fill it in with real logic:
<?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; use Illuminate\Support\Facades\Log; class SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** * The number of times the job may be attempted. */ public int $tries = 3; /** * The number of seconds to wait before retrying a failed job. */ public int $backoff = 60; /** * Create a new job instance. * Pass the User model in — SerializesModels handles it safely. */ public function __construct( public readonly User $user ) {} /** * Execute the job. * This runs in the background — take as long as you need. */ public function handle(): void { Log::info('Sending welcome email to: ' . $this->user->email); Mail::to($this->user) ->send(new WelcomeMail($this->user)); Log::info('Welcome email sent successfully to: ' . $this->user->email); } /** * Handle a job failure — called when all retries are exhausted. */ public function failed(\Throwable $exception): void { Log::error('SendWelcomeEmail failed for user ' . $this->user->id . ': ' . $exception->getMessage()); } }
Notice the job implements ShouldQueue — this is what tells Laravel to push it onto the queue instead of running it immediately. Without this interface, the job would execute synchronously.
When you pass an Eloquent model (like $user) to a job, Laravel doesn't store the entire object in the queue. It stores just the model's ID and re-fetches it fresh from the database when the worker runs the job. This prevents stale data and reduces queue payload size.
5 Step 3 — Dispatch the Job
Dispatching means pushing the job onto the queue. You can dispatch from a controller, a route, a model observer, a command — anywhere in your application.
Dispatching from a Controller
<?php namespace App\Http\Controllers; use App\Jobs\SendWelcomeEmail; use App\Models\User; use Illuminate\Http\Request; class AuthController extends Controller { public function register(Request $request) { // Create the user $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => bcrypt($request->password), ]); // ✅ Dispatch the job — returns INSTANTLY to the user SendWelcomeEmail::dispatch($user); return response()->json([ 'message' => 'Registration successful! Check your email.' ], 201); } }
The user gets a 201 Created response immediately. The email is sent asynchronously by the queue worker — no waiting.
Other dispatch methods
// Dispatch with a delay (process after 10 minutes) SendWelcomeEmail::dispatch($user) ->delay(now()->addMinutes(10)); // Dispatch to a specific named queue SendWelcomeEmail::dispatch($user) ->onQueue('emails'); // Dispatch conditionally SendWelcomeEmail::dispatchIf($user->email_verified, $user); // Dispatch using the helper function dispatch(new SendWelcomeEmail($user)); // Run synchronously RIGHT NOW (skips the queue — useful for testing) SendWelcomeEmail::dispatchSync($user);
6 Step 4 — Run the Queue Worker
Dispatching puts the job in the queue. Now you need a worker to actually process it. Open a new terminal window and run:
# Start the default queue worker php artisan queue:work # Process only the 'emails' queue php artisan queue:work --queue=emails # Process with priority: 'high' first, then 'default' php artisan queue:work --queue=high,default # Process a single job then stop (useful for cron jobs) php artisan queue:work --once # Listen mode — restarts after every job (slower but picks up code changes) php artisan queue:listen
You'll see output like this in your terminal as jobs are processed:
INFO Processing jobs from the [default] queue. 2026-04-04 10:23:01 App\Jobs\SendWelcomeEmail .......... RUNNING 2026-04-04 10:23:02 App\Jobs\SendWelcomeEmail ....... 2.45ms DONE
Use queue:work in production (faster, caches classes in memory). Use queue:listen in local development when you are actively changing job code, since it restarts the worker after each job and picks up code changes automatically.
7 Step 5 — Handle Failed Jobs
What happens when a job throws an exception? By default, Laravel retries it up to your configured $tries limit. After all retries are exhausted, it moves the job to the failed_jobs table.
Create the failed jobs table
# Create the failed_jobs migration (Laravel 12 may include this already) php artisan queue:failed-table php artisan migrate
Working with failed jobs
# View all failed jobs php artisan queue:failed # Retry a specific failed job by its ID php artisan queue:retry 5 # Retry ALL failed jobs at once php artisan queue:retry all # Delete a specific failed job php artisan queue:forget 5 # Clear all failed jobs from the table php artisan queue:flush
Configuring retry behaviour in your Job class
class SendWelcomeEmail implements ShouldQueue { // Maximum number of attempts before marking as failed public int $tries = 3; // Wait 60 seconds between retries (exponential: 60, 120, 180...) public int $backoff = 60; // Maximum seconds a single attempt may run before timing out public int $timeout = 120; // Only retry until this specific time public function retryUntil(): \DateTime { return now()->addHours(2); } }
8 Step 6 — Queue Priorities and Multiple Queues
As your application grows, you'll want some jobs to be processed before others. For example, password reset emails should be sent before a weekly newsletter. Use named queues to prioritise jobs.
// High priority — transactional emails SendPasswordResetEmail::dispatch($user) ->onQueue('high'); // Normal priority — welcome emails SendWelcomeEmail::dispatch($user) ->onQueue('default'); // Low priority — newsletters, reports SendWeeklyNewsletter::dispatch() ->onQueue('low');
Start the worker with priority order — it processes high first, then default, then low:
php artisan queue:work --queue=high,default,low9 Step 7 — Run Workers in Production with Supervisor
In production, you must not run php artisan queue:work manually. If your terminal closes, the worker stops and jobs pile up unprocessed. The solution is Supervisor — a Linux process manager that keeps your worker alive automatically, even after server reboots.
Install Supervisor
Run on your Ubuntu/Debian server: sudo apt-get install supervisor
Create a Supervisor config file
Create /etc/supervisor/conf.d/laravel-worker.conf with the configuration below.
[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/your-app/artisan queue:work database --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=4 redirect_stderr=true stdout_logfile=/var/www/your-app/storage/logs/worker.log stopwaitsecs=3600
Start Supervisor
Run these three commands to activate the configuration:
# Reload Supervisor configuration sudo supervisorctl reread sudo supervisorctl update # Start the laravel-worker group sudo supervisorctl start laravel-worker:* # Check status sudo supervisorctl status
You should see 4 worker processes (matching numprocs=4) all showing RUNNING. If a worker crashes, Supervisor restarts it automatically within seconds.
Because queue:work caches your code in memory, workers won't pick up code changes until restarted. Add this to your deployment script: php artisan queue:restart. Supervisor will gracefully restart all workers.
10 Real-World Example: Image Resize Queue
Let's look at another practical example — resizing a product image after upload. This is a perfect queue use case because image processing is CPU-intensive and should never block the web request.
php artisan make:job ResizeProductImage<?php namespace App\Jobs; use App\Models\Product; 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\Storage; use Intervention\Image\Facades\Image; class ResizeProductImage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 2; public int $timeout = 300; // 5 minutes max public function __construct( public readonly Product $product, public readonly string $imagePath ) {} public function handle(): void { $sizes = [ 'thumb' => [150, 150], 'medium' => [600, 600], 'large' => [1200, 1200], ]; foreach ($sizes as $name => [$w, $h]) { $resized = Image::make( Storage::path($this->imagePath) )->fit($w, $h)->encode('webp', 85); $path = "products/{$this->product->id}/{$name}.webp"; Storage::put($path, $resized); } $this->product->update(['images_processed' => true]); } }
Dispatch it from your product upload controller:
public function store(Request $request) { $path = $request->file('image')->store('uploads'); $product = Product::create($request->validated()); // Dispatch image resizing to the background ✅ ResizeProductImage::dispatch($product, $path) ->onQueue('images'); // User gets this response INSTANTLY — no waiting for image processing return response()->json([ 'message' => 'Product created! Images are being processed.', 'product' => $product, ], 201); }
11 Laravel Queues Beginner Checklist
Use this checklist to confirm you have set up queues correctly:
- QUEUE_CONNECTION=database set in .env
- php artisan migrate run — jobs table exists in database
- Job class implements ShouldQueue interface
- Job uses Dispatchable, InteractsWithQueue, Queueable, SerializesModels traits
- $tries and $timeout configured on the job
- failed() method defined to handle exhausted retries
- failed_jobs table created and migrated
- Queue worker running (queue:work or via Supervisor in production)
- php artisan queue:restart added to deployment script
- Supervisor configured in production with autorestart=true
12 Frequently Asked Questions
✓ Conclusion
Laravel queues are one of the most impactful features you can add to a web application. They transform slow, blocking operations into instant responses — making your app feel fast and professional from the very first interaction.
Here's what you've learned in this guide:
- The mental model: jobs, queues, and workers explained simply
- How to configure the database queue driver with zero extra dependencies
- Creating a fully-featured job class with retries, backoff, and failure handling
- Dispatching jobs with delays, priorities, and named queues
- Running and managing failed jobs from the CLI
- Deploying queue workers reliably in production using Supervisor
The next step is to identify the slowest operations in your own Laravel application — email sending, PDF generation, API calls, image processing — and move them into jobs. Your users will notice the difference immediately.
Need help building a scalable Laravel application with queues, jobs, and background processing? Contact Jahid Babu Tech for a free consultation.