Laravel 13 Queues Explained: Jobs, Workers, and Failed Jobs

Arlind Musliu cofounder at Lucky Media
Arlind Musliu

January 18, 2024 · 7 min read

Laravel for Beginners: Using Queues

2026 UPDATE - LARAVEL 13

We’re excited to announce that we have updated all of our blog post examples to reflect the new Laravel 13 version! Our previous examples were based on Laravel 10, but with the release of Laravel 13, we wanted to ensure that our readers have access to the most up-to-date information and examples.

Improved app performance with queues

Your site's performance can be just as important as the quality of your content. Slow load times or a sluggish interface can quickly turn readers away. That's where Laravel's queue system comes into play, helping you defer the execution of time-consuming tasks such as sending emails or processing images.

What are Laravel queues?

Laravel Queues provide a unified API for deferring tasks to a later time, allowing you to perform resource-intensive tasks in the background without affecting the user experience. By moving tasks such as email sending or feed processing to a queue, you can respond to user requests quickly, while the server works through the queued jobs at its own pace.

Setting up a queue

Laravel supports various queue backends like Redis, Amazon SQS, and database-driven queues. For beginners, the database queue driver is a good starting point as it doesn't require additional services.

To set up a database queue, first, create a migration for the jobs table:

php artisan queue:table
php artisan migrate

Next, update your .env file to use the database queue driver:

// Other options

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120

// Other options

Creating jobs

In Laravel, queued tasks are represented as "Jobs." You can generate a new job class using the command:

php artisan make:job SendNewPostNotification

This command will create a new job class in the app/Jobs directory. Within this class, you define the task you want to perform. Here's an example job that sends an email notification when a new post is published:

namespace App\Jobs;

use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendNewPostNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function handle()
    {
        $mailable = new Mailable(...); // Your mailable class with post details
        Mail::to('subscribers@example.com')->send($mailable);
    }
}

We explain the email process in detail in another article.

Laravel 13: PHP attributes for jobs

Laravel 13 introduces optional PHP 8 attributes as a modern alternative to defining job properties as class properties. Instead of setting $tries, $timeout, and $backoff as properties, you can annotate the job class directly:

use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\FailOnTimeout;

#[Tries(3)]
#[Timeout(60)]
#[Backoff(30)]
#[FailOnTimeout]
class SendNewPostNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // No need for $tries, $timeout, $backoff properties
    // The attributes above replace them

    public function handle(): void
    {
        // job logic here
    }
}

This is entirely optional. The traditional class properties still work and there is no functional difference. The attribute syntax is simply cleaner when working in modern PHP 8 codebases.

Dispatching jobs

Once you've created your job, dispatching it is simple. You can dispatch a job from anywhere in your application, such as from a controller method when a new blog post is created:

namespace App\Http\Controllers;

use App\Jobs\SendNewPostNotification;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function store(Request $request)
    {
		// Other stuff

        // Store the blog post...
        $post = Post::create($request->all());

        // Dispatch the job to send the notification
        SendNewPostNotification::dispatch($post);

        // Other stuff
    }
}

Running the queue worker

To process the jobs in your queue, you need to run the queue worker. You can start the worker with the command:

php artisan queue:work

For production environments, you should configure a process monitor to ensure that the queue worker does not stop running.

queue:work vs queue:listen

Laravel provides two commands for processing queued jobs. Understanding the difference is important for both local development and production deployments.

queue:work starts a long-lived process that loads the application once and keeps it in memory. Each job is processed by the same in-memory instance. This is faster and is the recommended option for production. The downside is that if you deploy new code, the worker must be restarted to pick up the changes:

php artisan queue:work

queue:listen starts a new PHP process for each job. This is slower because the framework bootstraps on every job, but it means code changes are picked up automatically without restarting the worker. It is useful during local development:

php artisan queue:listen

For production, always use queue:work and restart workers after each deployment with php artisan queue:restart.

Handling failed jobs

Sometimes jobs fail. Laravel provides a way to handle failed jobs by inserting them into a failed_jobs table. To create this table, run:

php artisan queue:failed-table
php artisan migrate

You can then define the maximum number of times a job should be attempted before being logged as failed:

php artisan queue:work --tries=3

Once a job has exceeded its retry limit, it is inserted into the failed_jobs table with the exception message and stack trace. You can inspect and retry failed jobs using Artisan commands:

# List all failed jobs
php artisan queue:failed

# Retry a specific failed job by its ID
php artisan queue:retry 5

# Retry all failed jobs
php artisan queue:retry all

# Flush (delete) all failed jobs
php artisan queue:flush

You can also define a failed method on the job class to run custom logic when the job ultimately fails, such as sending an alert or rolling back a database operation:

public function failed(\Throwable $exception): void
{
    // Send user notification, log the error, etc.
    \Log::error('SendNewPostNotification failed: ' . $exception->getMessage());
}

Running queue workers in production with supervisor

In production, the queue worker must keep running continuously. If it crashes, no jobs will be processed. The recommended tool for this is Supervisor, a process control system for Linux servers.

Install Supervisor on your server:

sudo apt-get install supervisor

Create a configuration file at /etc/supervisor/conf.d/laravel-worker.conf:

[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=2
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
stopwaitsecs=3600

After creating the config, reload Supervisor:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

Supervisor will restart the worker automatically if it exits. After deploying new code, run php artisan queue:restart to gracefully restart all workers so they pick up the new code.

laravel best framework php

Queue drivers: Which one to use?

Laravel supports several queue drivers. Here is a quick comparison to help you choose:

  • database - Stores jobs in a database table. No extra services needed. Good for small apps and beginners. Can become a bottleneck under high load.

  • redis - Fast, in-memory queue. Recommended for medium to large applications. Requires a Redis server.

  • sqs - Amazon Simple Queue Service. Highly scalable and managed. Best for AWS-hosted applications.

  • sync - Executes jobs immediately in the same process. Useful for local development and testing when you don't want a background worker running.

  • null - Discards all queued jobs. Useful in tests when you want to prevent jobs from running.

Testing queued jobs

Laravel provides a Queue::fake() helper that prevents jobs from actually being pushed to the queue during tests. You can then assert that a job was dispatched:

use Illuminate\Support\Facades\Queue;
use App\Jobs\SendNewPostNotification;

public function test_notification_job_is_dispatched_on_post_creation(): void
{
    Queue::fake();

    // Trigger the action that should dispatch the job
    $this->post('/posts', ['title' => 'Hello World', 'content' => 'Content here']);

    // Assert the job was dispatched
    Queue::assertDispatched(SendNewPostNotification::class);

    // Assert the job was dispatched with specific data
    Queue::assertDispatched(SendNewPostNotification::class, function ($job) {
        return $job->post->title === 'Hello World';
    });
}

You can also use Queue::assertNotDispatched() to verify a job was not pushed, and Queue::assertNothingDispatched() to assert no jobs were dispatched at all during the test.

Conclusion

Laravel Queues are a powerful feature that can significantly improve the responsiveness and efficiency of your application. By offloading time-consuming tasks to the background, you can ensure that your readers enjoy a fast and seamless experience on your site.

Upcoming Articles in the Series

  1. Laravel for Beginners: Sending Emails

  2. Laravel for Beginners: Localization and Languages

  3. Laravel for Beginners: Query Performance Issues and Debugbar

This article is part of our series Laravel for Beginners: A Step-by-Step Guide to Learn the Concepts.


Bring Your Ideas to Life 🚀

If you need help with a Laravel project let's get in touch.

Lucky Media is proud to be recognized as a leading Laravel Development Agency

Frequently Asked Questions

What is the difference between queue:work and queue:listen?

queue:work boots the application once and keeps it in memory for all subsequent jobs, it is faster and recommended for production. queue:listen spawns a fresh PHP process for every job, which is slower but automatically picks up code changes without restarting. Use queue:work in production and queue:listen during development if you need auto-reload behaviour.

How do I handle failed jobs in Laravel?

Run php artisan queue:failed-table && php artisan migrate to create the failed_jobs table. Laravel will store failed jobs there automatically once a job exceeds its retry limit. Use php artisan queue:failed to list them, php artisan queue:retry {id} to retry one, and php artisan queue:retry all to retry all. You can also define a failed(Throwable $exception) method on the job class to run custom cleanup logic.

Which queue driver should I use for a small Laravel app?

The database driver is the easiest to get started with - it requires no extra infrastructure and stores jobs in your existing database. Set QUEUE_CONNECTION=database in your .env and run php artisan queue:table && php artisan migrate. Once your application grows, migrate to Redis for better throughput.

How do I test queued jobs in Laravel?

Use Queue::fake() at the start of your test to prevent jobs from actually being dispatched to a worker. Then trigger the action under test and use Queue::assertDispatched(MyJob::class) to verify the job was pushed. You can also pass a closure to assert on specific job properties. For jobs you want to actually execute during a test, set QUEUE_CONNECTION=sync in your test environment.

Technologies

Laravel
Arlind Musliu cofounder at Lucky Media
Arlind Musliu

Cofounder and CFO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork