Skip to content

Laravel

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.

Updated View as Markdown

Queen 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. In exchange you get a backlog that is on disk (and in a cluster on a majority of nodes) before dispatch() returns, an ordered lane per entity when you want one, and workers that do more with less. In our runs of 2026-10-01 on one 16-vCPU Linux server, with every write fsynced on both sides, 32 workers completed 2,753 jobs/s of 10 ms jobs against 1,124 for Horizon, and an application with 64 forked workers fit in 183 MiB against 1,892 (benchmark).

Install

  1. Start a broker if you have none yet. One container is enough to try it (quickstart):

    docker run -d --name queen --platform linux/amd64 -p 6632:6632 \
      -v queen-data:/var/lib/queen/raft ghcr.io/queen-mq/queen:latest
  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.

    composer require queen-mq/php-client
    php artisan vendor:publish --tag=queen-config
  3. Point Laravel at Queen.

    QUEUE_CONNECTION=queen
    QUEEN_URL=http://127.0.0.1:6632
    QUEEN_QUEUE=default
    QUEEN_CONSUMER_GROUP=laravel
    QUEEN_RETRY_AFTER=90
  4. Dispatch a job you already have. Nothing changes at the call site.

    GenerateInvoice::dispatch($invoiceId);
  5. Run a worker.

    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.

QUEEN_RETRY_AFTER 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. 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, 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), 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; the supervisors need pcntl and posix on Unix.

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 UUID. A job that implements QueenPartitionable names its own partition instead:

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. You need no lock or version column for it, because the broker leases a partition to one worker at a time, and the partition is created by the first job that names it (how it works).

Choose a delivery profile

The safe default

prefetch=1 and ack_batch=1 keep one synchronous acknowledgement per job: the worker asks for a job, runs it, and waits until the broker has stored its ACK. Start here while you validate a migration, and stay here whenever every job, with a margin, fits inside retry_after.

QUEEN_PREFETCH=1
QUEEN_ACK_BATCH=1

Turn on lease renewal even with prefetch=1 when you cannot bound a job’s runtime. The lease is then renewed while the job runs, and a worker that can no longer renew in time is stopped before its lease ends, so a job never runs in two workers at once. Under the Rust supervisor on Linux the master renews the leases of all its workers; anywhere else each worker starts one small PHP helper beside it. That path needs pcntl and posix, and your code must not replace the driver’s signal handler.

A faster profile

Every write a worker waits for is an fsync on the broker, so the round trips decide how many short jobs a worker can run. Three settings take them off the worker’s path. prefetch leases several jobs with one pop. ack_async sends the ACK of a finished job and starts the next one without waiting for the answer. pop_ahead sends the pop for the next batch while the last job of a full batch runs.

QUEEN_PREFETCH=4
QUEEN_ACK_BATCH=1
QUEEN_LEASE_RENEWAL=true
QUEEN_ACK_ASYNC=true
QUEEN_POP_AHEAD=true

On the Linux server, 32 workers at prefetch 4 went from 1,879 to 2,794 jobs/s with the other two (benchmark). The price is a wider at-least-once window. The connector refuses prefetch above 1, and pop_ahead, without QUEEN_LEASE_RENEWAL=true, because Laravel can pause a worker (maintenance mode, queue:pause) while it still holds leased jobs. ack_async needs ack_batch=1, since a batch already defers its ACKs. When the broker refuses an asynchronous ACK, the worker learns it one job later, reports it through Laravel’s exception handler, abandons the lease and drops the rest of its batch, and the refused job runs again after its lease expires.

A worker pops ahead only after a full batch: a short batch means the queue was nearly empty, and a pop sent ahead would come back empty too. It never pops ahead after a job that took more than a third of retry_after, nor when it serves a comma-separated queue list, whose next job may come from another queue. When a worker crashes, whatever renews its lease hands back the prefetched jobs it had not started, without an extra attempt; keep tries at 2 or more on these connections anyway, for the crashes that cannot be handed back (prefetch). ack_async, pop_ahead and the crash hand-back need PHP client 1.9.0.

What Laravel keeps

Laravel With Queen
dispatch(), middleware, timeout, tries, backoff unchanged
Delayed dispatch a Queen timer holds the job until it is due
release() ACK and re-push, or ACK and timer, in one transaction
failed_jobs and its commands unchanged; the broker keeps a dead-letter copy in step (QUEEN_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, on both supervisors (the list).

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 the same optional features beyond Horizon’s pools: coordinated replicas, prefork workers, event-driven scaling, per-queue minimums and fast scale-up. Start with worker supervisors, and turn on the dashboard once authentication is in place.

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 1.9.0. Everything not listed here works from 1.6.0 on.

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

Limits

Delivery is at-least-once, as it is on Redis: a lease expiry, a fenced worker or a crash can run a job twice, so a job with an external effect needs an idempotency key (charge a card once).

A job waits for the job ahead of it in its partition. A long job therefore delays the short jobs that hashed to its stripe, even while other workers are idle, so give long jobs a queue of their own. A released job goes to the back of its partition, behind jobs dispatched after it for the same entity.

Run one supervisor master per application and consumer group, unless every replica runs with coordination: the state directory’s lock only excludes a second master on the same host. The supervisors and the dashboard are previews and run on Unix only. The Rust engine’s Linux builds are static and their release qualification is still pending, and there is no Windows build.

In 2.0.0-beta.6 the broker’s depth read does not decode a percent-encoded queue name, so a queue named with a space, a colon or a slash looks empty to the supervisor, which then never scales it up. Keep queue names to letters, digits, -, _ and ..

Horizon still does some things Queen does not: silenced jobs, job lists, batches in its dashboard, Slack and SMS alert routes, per-supervisor controls, and clearing a queue.

Read next: how Queen runs Laravel jobs, then Queen or Horizon and migrate from Horizon.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close