# Async Job Processing with Laravel Queue and Supervisor

> How to set up asynchronous job queues in Laravel using the database driver, and keep workers running reliably in production with Supervisor.

- Published: 2023-07-02
- Updated: 2026-10-07
- Category: Framework & Library
- Tags: PHP, Laravel
- Reading time: 4 min read
- Source: https://muhammetsafak.com/blog/async-job-processing-with-laravel-queue-and-supervisor/
- Language: en-US
- Author: Muhammet Şafak

---
> **TL;DR:** Laravel's default queue driver, sync, runs jobs inline, so Queue gives no benefit until you switch drivers. This post sets up the database driver, runs workers under Supervisor and restarts them after each deploy so they do not keep running old code.

A web request has one job: respond to the user fast. But some operations are inherently slow — sending an email, processing an image, waiting on a third-party API. If you handle those inside the request-response cycle, the user sits there waiting for your slow work to finish. Laravel Queue exists exactly for this: push the work onto a queue now, return the response immediately, and do the actual work in the background.

In this post I walk through how I set up a job queue in Laravel and how I use Supervisor to keep it running in production.

## Which queue driver should you choose?

Laravel Queue supports multiple drivers — database, Redis, Amazon SQS, and more. The driver determines where queued jobs are stored. The `QUEUE_CONNECTION` key in your `.env` file controls this, and [as the Laravel 10.x queue documentation states](https://laravel.com/docs/10.x/queues#connections-vs-queues), `sync` is the default driver in new applications — meaning jobs are never queued at all; they run inline immediately. That's convenient during development, but in production you'll see zero benefit from Queue until you switch to a real driver.

In this post I'm using the database driver. It's sufficient for small to medium workloads and requires no additional infrastructure. Jobs are stored in a `jobs` table; [as the documentation describes](https://laravel.com/docs/10.x/queues#database), to create it:

```bash
php artisan queue:table
php artisan migrate
```

## Defining a job

Every piece of work pushed onto the queue is a class. As an example, I'll create a `SendMailJob` that sends an email in the background:

```bash
php artisan make:job SendMailJob
```

I fill out the generated class, clarifying what parameters it accepts and what it does:

```php
<?php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

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

    public function __construct(
        private string $name,
        private string $mail,
    ) {}

    public function handle(): void
    {
        $subject = 'Merhaba ' . $this->name;
        $msg = 'Sitemize üye olduğunuz için teşekkür ederiz!';
        if (!mail($this->mail, $subject, $msg)) {
            throw new \Exception('Mail gönderilemedi.');
        }
    }
}
```

Two points matter here. First: the data I receive in `__construct` is stored as [class properties](/blog/php-7-4-typed-properties-and-arrow-functions/) — Laravel serializes this data and holds it in the queue, then restores it when the job runs. For that reason, it's good practice to pass simple values (IDs, short strings) to the constructor rather than large objects. Second: inside `handle`, I throw an exception when the job fails. This is not optional. [The documentation puts it plainly](https://laravel.com/docs/10.x/queues#dealing-with-failed-jobs): if an exception is thrown while the job is being processed, the job is automatically released back onto the queue and retried until it hits the maximum number of allowed attempts. There is one other way to put a job back — calling `release()` — but a job that silently `return`s does neither: the queue considers it finished, so it counts as successful even when it failed.

Dispatching a job is a single line:

```php
\App\Jobs\SendMailJob::dispatch('Muhammet', 'info@muhammetsafak.com');
```

## Processing jobs

Jobs pushed onto the queue don't run by themselves; you need a worker to process them:

```bash
php artisan queue:work
```

This command continuously listens to the queue and processes jobs as they arrive. If you want to separate jobs by priority, you can use named queues with `onQueue()` and point the worker at those queues:

```php
\App\Jobs\SendMailJob::dispatch($name, $mail)->onQueue('medium');
```

```bash
php artisan queue:work --queue=high,medium,default
```

The order here determines priority: [the documentation](https://laravel.com/docs/10.x/queues#queue-priorities) says a worker started this way "gives them higher processing priority" for jobs on the `high` queue.

## Why do queue workers need Supervisor?

`queue:work` is a long-running process — and long-running processes die. An exception, a memory limit, a deployment… the worker stops, and if nobody notices, the queue silently backs up. In production, that's unacceptable.

Supervisor is a tool on Unix systems that monitors background processes and restarts them when they stop. That's exactly what I want for Laravel workers.

```bash
sudo apt-get update
sudo apt-get install supervisor -y
```

I create a configuration file under `/etc/supervisor/conf.d/`:

```ini
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /proje/yolu/artisan queue:work --queue=default --tries=3
autostart=true
autorestart=true
user=deploy
numprocs=8
redirect_stderr=true
stdout_logfile=/proje/yolu/storage/logs/worker.log
stopwaitsecs=3600
```

A few lines are critical. `autorestart=true` brings the worker back when it dies. `numprocs=8` runs eight parallel workers — tune that number to your load. `stopwaitsecs=3600` gives a running job up to an hour to finish during a deployment, so it isn't cut off mid-execution: [per the Supervisor documentation](https://supervisord.org/configuration.html#program-x-section-settings) this is the number of seconds to wait for a process to shut down after it has been sent a stop signal, and it defaults to 10 — which would kill an hour-long job in ten seconds. [Laravel's own example configuration](https://laravel.com/docs/10.x/queues#supervisor-configuration) uses 3600 for the same reason. To register and start the configuration:

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

## One final note

When you push a code change and deploy, the running workers continue holding the old code in memory. [The documentation warns about this under its own heading](https://laravel.com/docs/10.x/queues#queue-workers-and-deployment): because workers are long-lived processes, they will not notice changes to your code without being restarted. Add `php artisan queue:restart` to your deployment steps after every deploy — otherwise you'll spend hours wondering why "I updated the code but it's still behaving the old way." I learned that one the hard way, more than once.

Laravel Queue and Supervisor together provide a simple but solid async processing layer: Laravel defines and dispatches jobs, and Supervisor ensures the processes that handle those jobs never stop running. Each is incomplete without the other; the value is in how they work together.

Once a queue retries failed jobs, as the `--tries=3` in the Supervisor configuration above does, the same job can run more than once. That is where the "Retries Done Right" and "Idempotency" chapters of my book [Designing Reliable Event-Driven Systems](/books/designing-reliable-event-driven-systems/) pick up.

---

### Sources

- [Laravel 10.x: Queues](https://laravel.com/docs/10.x/queues)
- [Laravel 10.x: Queues — Dealing With Failed Jobs](https://laravel.com/docs/10.x/queues#dealing-with-failed-jobs)
- [Laravel 10.x: Queues — Supervisor Configuration](https://laravel.com/docs/10.x/queues#supervisor-configuration)
- [Laravel 10.x: Queues — Queue Workers & Deployment](https://laravel.com/docs/10.x/queues#queue-workers-and-deployment)
- [Supervisor: [program:x] Section Settings](https://supervisord.org/configuration.html#program-x-section-settings)

## Frequently asked

### Which queue driver should you choose?

Laravel Queue supports database, Redis, Amazon SQS and more, set through the QUEUE_CONNECTION key. The default in new applications is sync, which runs jobs inline, so Queue gives no benefit in production until you switch to a real driver. I use the database driver for small to medium workloads because it needs no additional infrastructure.

### Why do queue workers need Supervisor?

queue:work is a long-running process, and long-running processes die: an exception, a memory limit, a deployment. If nobody notices, the queue silently backs up. Supervisor monitors background processes on Unix systems and restarts them when they stop.

### What do the key Supervisor settings do?

autorestart=true brings a worker back when it dies. numprocs=8 runs eight parallel workers, and you should tune that number to your load. stopwaitsecs=3600 gives a running job up to an hour to finish during a deployment. Supervisor's default is 10 seconds, which would kill an hour-long job.

### What should you do after deploying new code?

Add php artisan queue:restart to your deployment steps. Workers are long-lived processes that keep the old code in memory, so without a restart you spend hours wondering why you updated the code but the behavior is still the old one.

### How does Laravel know a job failed?

Throw an exception inside handle when the job fails. That is not optional: an exception makes the job go back to the queue and be retried until the allowed number of attempts is reached. A job that silently returns is considered finished, even if it actually failed.

### How do you prioritize jobs?

Use named queues with onQueue() and start the worker with an ordered list, such as --queue=high,medium,default. The order determines priority, and jobs on the high queue get higher processing priority.
