# 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-09-07
- Category: Framework & Library
- Tags: PHP, Laravel
- Reading time: 4 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/async-job-processing-with-laravel-queue-and-supervisor/
- Language: en-US
- Author: Muhammet Şafak

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

## Choosing a queue driver

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](/en/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.tr');
```

## 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 says so explicitly](https://laravel.com/docs/10.x/queues#queue-priorities) — every job on the `high` queue is processed before the worker moves on to `medium`.

## Supervisor: keeping workers alive

`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.

---

### 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)
