---
title: "Laravel queue driver: install and run your jobs"
description: "Run your Laravel jobs on Queen: install the queue connection, keep your job classes and workers, order jobs per entity, pick a delivery profile, and choose who runs the workers."
---

> Queen MQ documentation, for AI agents
> Complete self-contained summary of Queen MQ: https://queenmq.com/llms-brief.txt
> Fetch that first when the question is about the product rather than about this page.
> Index of all pages: https://queenmq.com/llms.txt

# Laravel queue driver: install and run your jobs

QueenMQ can take over two jobs in a Horizon setup: holding the jobs, which Redis does today, and running the workers, which Horizon's master does. Your job classes stay ordinary Laravel jobs, and `queue:work` stays the worker.

**Why:** a job in Redis can be lost with the Redis process, and a Horizon master boots Laravel in every worker. With QueenMQ the backlog is on disk (in a cluster, on a majority of nodes) before `dispatch()` returns, an entity can have its own ordered lane, and forked workers share one booted framework. In our runs on one 16-vCPU server, 32 workers completed 2,753 jobs a second of 10 ms jobs against Horizon's 1,124, and 64 forked workers fit in 183 MiB against 1,892 ([benchmark](/benchmarks/laravel/)).

If you run Horizon today, [Queen or Horizon](/integrations/laravel/queen-vs-horizon/) compares the two, and [migrate from Horizon](/integrations/laravel/migrate-from-horizon/) moves a running application step by step.

## Install

1. **Start a broker** if you have none yet. One container is enough to try it
   ([quickstart](/writing-applications/)):

```bash
docker run -d --name queen -p 6632:6632 \
  -v queen-data:/var/lib/queen/raft ghcr.io/queen-mq/queen:2.6.0
```

2. **Install the package and publish its configuration.** Package discovery registers the service
   provider, the `Queen` facade and a `queen` queue connection, so `config/queue.php` needs no
   entry unless you want to override it.

```bash
composer require queen-mq/php-client
php artisan vendor:publish --tag=queen-config
```

3. **Point Laravel at Queen.**

```dotenv
QUEUE_CONNECTION=queen
QUEEN_URL=http://127.0.0.1:6632
QUEEN_QUEUE=default
QUEEN_CONSUMER_GROUP=laravel
```

4. **Dispatch a job you already have.** Nothing changes at the call site.

```php
GenerateInvoice::dispatch($invoiceId);
```

5. **Run a worker.**

```bash
php artisan queue:work queen --queue=default --timeout=60 --tries=3
```

That is a working integration, with no supervisor. Keep your current systemd, Kubernetes or
Supervisor definition if a fixed number of workers is all you need, and stop a worker with the
signal you already use.

`retry_after` in `config/queen.php`, 90 seconds by default, is the Queen lease: how long a worker
holds a job before the broker gives it to someone else. It must be longer than the worker's
`--timeout` and than the slowest job, unless you turn on
[lease renewal](/integrations/laravel/concepts/#lease-renewal-and-fencing). Laravel does not pass
`queue:work --timeout` to the connection, so without lease renewal the driver checks only a job's
own `$timeout` against the lease. A Queen supervisor refuses a pool whose `timeout` is not shorter
than `retry_after`; under any other process manager, keep `--timeout` below `retry_after`
yourself.

The consumer group is the name under which your workers share the work: two applications in one
group split the jobs between them, and two groups on one queue each get every job, because the
driver subscribes each new group to everything the queue holds. Give each application and
environment its own queue names (or its own broker) and its own group.

When the broker requires a credential, set `QUEEN_BEARER_TOKEN`: a broker JWT with the
`read-write` role, or, through the broker's [embedded proxy](/manage/security/), an API key with
the `produce`, `consume` and `read` scopes. The proxy classifies the deletion of a dead-letter
entry as queue administration, so with failed-job sync on (the default) that key needs the `admin`
scope too, or `queue:retry` and `queue:forget` fail before they touch Laravel's row. Behind the
proxy the cluster's plan also caps the partitions per queue (8 on `free`, 64 on `dedicated-s`, see
[tenants](/manage/tenants/)), so keep `QUEEN_PARTITIONS` within it, and remember that every entity
a `QueenPartitionable` job names is a partition too.

You need PHP 8.3 or later. The queue connection needs no extension unless you turn on
`push_compression` or `framed_pop`, which need `zstd`; the supervisors need `pcntl` and
`posix` on Unix. Every key of `config/queen.php`, its default and the 20 environment variables the
file reads are in the [configuration reference](/integrations/laravel/configuration/).

That is a working integration, with no supervisor: keep your systemd, Kubernetes or Supervisor definition if a fixed number of workers is all you need.

Three settings to know from the first day:

- **`retry_after`** (90 seconds) is the lease: how long a worker holds a job before the broker gives it to someone else. Keep it longer than the worker's `--timeout` and the slowest job, or turn on [lease renewal](/integrations/laravel/concepts/#lease-renewal-and-fencing). Laravel does not pass `--timeout` to the connection, so keep the two in step yourself; a Queen supervisor refuses a pool whose `timeout` is not shorter than `retry_after`.
- **The consumer group** is the name under which your workers share the work. Two applications in one group split the jobs; two groups on one queue each get every job. Give each application and environment its own queues and its own group.
- **`QUEEN_BEARER_TOKEN`**, when the broker asks for a credential: a JWT with the `read-write` role, or a [proxy](/manage/tenants/) API key with `produce`, `consume`, `read`, and `admin` for `queue:retry` and `queue:forget`.

You need PHP 8.3 or later; the supervisors need `pcntl` and `posix`, and the binary forms need `zstd`. Every setting is in the [configuration reference](/integrations/laravel/configuration/).

## Order jobs per entity

By default the driver spreads jobs over 64 partitions of the queue (`laravel-0000` to `laravel-0063`), by a hash of each job's id. A job that implements `QueenPartitionable` names its own partition:

```php
use Illuminate\Contracts\Queue\ShouldQueue;
use Queen\Laravel\Contracts\QueenPartitionable;

final class RebuildCustomer implements ShouldQueue, QueenPartitionable
{
    public function __construct(public string $customerId) {}

    public function queenPartition(): string
    {
        return 'customer:'.$this->customerId;
    }
}
```

Every `RebuildCustomer` of one customer now runs in dispatch order, one at a time, and customers never wait for each other. The [ordering example](/integrations/laravel/examples/ordering/) runs it on four workers and checks the order.

**Why:** "the jobs of one customer, in order" usually means a lock or a version column. Here the broker leases a partition to one worker at a time, so the order is the queue's own, and the partition is created by the first job that names it.

## Choose a delivery profile

Three profiles cover most applications. They differ in how many round trips to the broker a worker waits for: every write a worker waits for is an fsync on the broker.

| | Safe | Balanced | Fast |
| --- | --- | --- | --- |
| Settings | `prefetch` 1 | `prefetch` `'auto'`, `lease_renewal` | as balanced, plus `ack_async` and `pop_ahead` |
| Empty jobs, 32 workers | 3,131 jobs/s | 5,681 jobs/s | 5,994 jobs/s |
| 10 ms jobs, 32 workers | 1,548 jobs/s | 2,003 jobs/s | 2,867 jobs/s |
| 200 ms jobs, 32 workers | 139 jobs/s | 139 jobs/s | 147 jobs/s |
| p99 at a steady 500 jobs/s | 29 ms | 30 ms, with `sleep` 0 | 29 ms |
| Use it for | a migration, strict one-job-at-a-time | most queues | short jobs, high volume |

Medians of three runs on the Linux server, with the Rust supervisor and prefork ([benchmark](/benchmarks/laravel/#delivery-profiles-and-prefetch-auto)).

### Safe

One acknowledgement per job: the worker asks for a job, runs it, and waits until the broker stored its ack. Start here while you validate a migration.

```php
// config/queen.php
'prefetch' => 1,
'ack_batch' => 1,
```

### Balanced

`prefetch` `'auto'` sizes each pop from how long the jobs take: about 250 ms of work per batch, up to 16 short jobs, one long job. With short jobs a worker pops about once every 15 jobs; with jobs of 200 ms or more it keeps one per pop, so it never holds work another worker could run.

```php
// config/queen.php
'block_for' => 1,
'prefetch' => 'auto',
'lease_renewal' => true,
// in each pool under supervisor.supervisors
'sleep' => 0,
```

With `sleep` 0 an idle worker waits on the broker (`block_for`) instead of sleeping, so it takes a job the moment one arrives: Laravel's `sleep` 1 left a burst to the few awake workers, at a p99 of 338 ms in one run. A crash the lease renewer cannot hand back delivers the rest of the batch again, so keep `tries` at 2 or more.

### Fast

Balanced, plus the two settings that take the last round trips off the worker's path: `ack_async` sends the ack and starts the next job without waiting for the answer, and `pop_ahead` sends the next pop while the last job of a full batch runs.

```php
// config/queen.php
'block_for' => 1,
'prefetch' => 'auto',
'lease_renewal' => true,
'ack_async' => true,
'pop_ahead' => true,
// in each pool under supervisor.supervisors
'sleep' => 0,
```

The price is a wider at-least-once window. The connector refuses `prefetch` above 1 and `pop_ahead` without `lease_renewal`, and `ack_async` needs `ack_batch` 1. A refused asynchronous ack is learned one job later, reported through Laravel's exception handler, and that job runs again after its lease. Details are in [prefetch, ack_async and pop_ahead](/integrations/laravel/concepts/#prefetch-ack_async-and-pop_ahead).

**Why:** a worker that waits for the broker after every job spends most of a short job's time waiting. Batching and sending ahead remove the waits; the lease renewer is what keeps a crash from holding the batch hostage.

## What Laravel keeps

| Laravel | With Queen |
| --- | --- |
| `dispatch()`, middleware, `timeout`, `tries`, `backoff` | unchanged |
| Delayed dispatch | a Queen [timer](/writing-applications/kv-locks-timers/) holds the job until it is due |
| `release()` | ACK and re-push, or ACK and timer, in one [transaction](/writing-applications/transactions/) |
| `failed_jobs` and its commands | unchanged; the broker keeps a dead-letter copy in step (`sync_failed_jobs`) |
| `Queue::size()`, `pendingSize()`, `reservedSize()` | the consumer group's depth in the broker, plus pending timers |
| Ordering | queue names, plus `QueenPartitionable` for an ordered lane per entity |
| Delivery | at-least-once, as on Redis: a lease expiry or a crash can run a job again |
| `queue:clear` | not supported: the command fails and the jobs stay |

All 32 Laravel queue features we checked (chains, batches, unique jobs, rate limits, middleware, encrypted jobs, queued listeners and notifications, the failed-job commands) behave as the Laravel documentation says ([the list](/benchmarks/laravel/#laravel-queue-features)).

## Who runs the workers

| Choice | Use it when | Master process |
| --- | --- | --- |
| Your process manager | a fixed number of `queue:work` processes is enough | whatever you run today |
| Queen PHP supervisor | you want Horizon-like pools and no native binary | Laravel stays loaded in one PHP master |
| Queen Rust supervisor | you want the smallest master | Rust, after one temporary Artisan run |

Both supervisors read the same `supervisor` section of `config/queen.php`, publish the same status and obey the same commands, and both offer, beyond Horizon's pools, [coordinated replicas](/integrations/laravel/supervisors/#several-replicas), [prefork workers](/integrations/laravel/supervisors/#prefork-workers) and [event-driven scaling](/integrations/laravel/supervisors/#event-driven-scaling). Start with [worker supervisors](/integrations/laravel/supervisors/), then the [dashboard](/integrations/laravel/dashboard/), [Kubernetes](/integrations/laravel/kubernetes/) and [monitoring](/integrations/laravel/monitoring/). Read the [production checks](/integrations/laravel/supervisors/#before-production) before you replace Horizon.

## Versions

The PHP client is `queen-mq/php-client` on Packagist; the Rust supervisor is a separate binary whose version each client release pins. These pages describe PHP client 2.5.0, which pins supervisor 0.8.1, published for Linux (amd64, arm64) and macOS with a signed manifest. Everything not listed works from 1.6.0.

| PHP client | Supervisor | Brings |
| --- | --- | --- |
| 1.7.0 | 0.4.0 | coordinated replicas, prefork, per-queue minimum, fast scale-up, job metrics, monitored tags, long-wait alerts, the Prometheus endpoint |
| 1.8.0 | 0.5.0 | event-driven scaling |
| 1.9.0 | 0.6.0 | lease renewal in the Rust master, `ack_async`, `pop_ahead`, the cURL transport, unstarted and crashed batches handed back without an attempt, job timeouts no longer counted as crashes, the dashboard's Retry now, In the queue now and tuning advice |
| 2.0.0 | 0.6.0 | every transaction ACK names its own lease; the dashboard's Jobs page works with a read-only token |
| 2.1.0 | 0.7.0 | `config/queen.php` reads 20 environment variables instead of 95 ([upgrading](/integrations/laravel/configuration/#upgrading-from-200)); `supervisor.lease_service` replaces `QUEEN_SUPERVISOR_LEASE_SERVICE`; with prefork, `queue:restart` runs the deployed code, and a fork server that is not safe to fork is not used |
| 2.2.0 | 0.7.0 | `prefetch` `'auto'` sizes each pop from the jobs' runtime; the safe, balanced and fast profiles |
| 2.3.0 | 0.8.0 | a pool's own `prefork` ([prefork for some pools only](/integrations/laravel/supervisors/#prefork-for-some-pools-only)); `queen:consume` sets and renews its lease, stops on `--idle-timeout`, and reports refused acks ([consume Queen messages](/integrations/laravel/consume/)) |
| 2.3.1 | 0.8.0 | `->supervision([...])` on a fluent `consume()->execute()` reports that consumer to the dashboard, off by default ([consumer status](/reference/supervisor-status/#optional-sdk-consumer-supervision)) |
| 2.4.0 | 0.8.0 | `$queen->lock()` and `$queen->semaphore()`, `->guard($lock)` on a transaction and `kv()->check()` ([locks](/writing-applications/kv-locks-timers/)); they need a 2.1 broker |
| 2.4.1 | 0.8.0 | a forked worker works its pool's connection; a pool's settings reach the worked connection alone, and a pool takes its connection's queue and consumer group; a worker forked after `queue:restart` leaves at once; failed jobs of other drivers stay Laravel's; the switch variables take `1` |
| 2.4.2 | 0.8.0 | a supervised worker that cannot consume leaves, or `queen:supervisor status` reports it after a minute ([inspect and control](/integrations/laravel/supervisors/#inspect-and-control-either-engine)); a delivery that carries no Laravel job goes to the dead-letter queue at once; refused installations and states name their owner, and `queen:supervisor-install --owner`; job metrics count timed-out attempts and `fail()` as failed |
| 2.4.3 | 0.8.1 | a worker that has not consumed counts from the start of its first failed pop, so a broker that stops answering shows once that pop times out; supervisor 0.8.1 stops stray workers, tries prefork again at `queue:restart`, reaps a dead fork server's workers and closes a circuit only with the probe |
| 2.5.0 | 0.8.1 | `binary_push`, `push_compression` and `framed_pop`, off by default ([the queue driver's settings](/integrations/laravel/configuration/#queue-driver)); the first two need a 2.3 broker on every node, and compression and framed pops need the zstd extension |

## Limits

- **Delivery is at least once**, as on Redis: a lease expiry or a crash can run a job twice, so a job with an external effect needs an idempotency key.
- **A job waits for the job ahead of it in its partition.** A long job delays the short jobs that hashed to its partition, even while other workers are idle: give long jobs a queue of their own. A released job goes to the back of its partition.
- **One supervisor master per application and consumer group**, unless every replica runs with coordination.
- **The supervisors and the dashboard are previews**, on Unix only.
- **Horizon still does things QueenMQ does not:** silenced jobs, job lists, batches in its dashboard, Slack and SMS alerts, per-supervisor controls, clearing a queue (`queue:clear` fails and the jobs stay).

Messages other services push to QueenMQ, which are not Laravel jobs, go to a class of yours with [`php artisan queen:consume`](/integrations/laravel/consume/).

Source: https://queenmq.com/integrations/laravel/index.mdx
