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
Common use cases: sending welcome emails after registration, processing CSV imports, generating reports, resizing uploaded images, syncing data with third-party APIs, and sending notifications via WhatsApp Cloud API or SMS.

Laravel 12 Queue and Jobs Tutorial: Background Processing Made Easy

Table Of Content

1 Prerequisites

Before starting, make sure you have:
  • 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)
If you're upgrading from Laravel 11, check out our Laravel 11 to 12 upgrade guide to ensure a smooth transition before setting up queues.

2 Introduction

Modern web applications must handle time-consuming tasks such as sending emails, processing files, generating reports, or syncing APIs — without slowing down user experience. Laravel 12 Queues and Jobs provide an elegant way to offload heavy tasks into the background while keeping your application fast and responsive.

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

Let's start by creating a fresh Laravel 12 project and configuring the database and queue driver. If you already have an existing Laravel 12 project, you can skip to Step 4.

3.1 Install Laravel Project

First, ensure Composer is installed on your system. Use the following command to install a new 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)

Open the .env file and input the necessary database credentials:

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

If using the database driver, generate the jobs table migration. Laravel 12 may include this by default, but run the command to be safe:
    
    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

Let's build a real example: sending a welcome email after user registration — but in the background.
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)

Assuming default Breeze/registration setup,
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

Start processing jobs:
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
For production, use Supervisor (example config /etc/supervisor/conf.d/laravel-worker.conf):
    
       [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

When a job exceeds its maximum retry attempts, Laravel automatically moves it to the failed_jobs table. Here's how to manage failures:

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-7b4fef4e5ece

Flush 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

  1. Run php artisan serve to start the development server
  2. Open a separate terminal and run: php artisan queue:work
  3. Register a new user at /register
  4. Check your mail trap (Mailpit, Mailhog, or set MAIL_MAILER=log in .env to log emails to storage/logs/laravel.log)
  5. 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;
        });
    
If you found this queue tutorial helpful, explore these related Laravel 12 guides to deepen your skills:

12 Conclusion

Laravel 12 queues make background processing straightforward yet powerful. By moving heavy tasks like email sending, file processing, and API calls off the HTTP request cycle, you create faster, more reliable applications that scale effortlessly.

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.
Revathi M - PHP and CodeIgniter Developer

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.