Skip to content

Queen or Horizon

For a team that runs Horizon: the two stacks side by side, where each one is ahead, what happened when we broke both on purpose, and the limits of that evidence.

Updated View as Markdown

Horizon and Queen both run ordinary Laravel jobs and both supervise their workers, on different backends: Horizon on Redis, Queen on its own broker. This page puts the two whole stacks side by side for a team that runs Horizon today. We are proud of where Queen comes out ahead, and we say as plainly where it does not. How Queen runs Laravel jobs defines the terms, and the Laravel benchmark has every run behind the numbers.

At a glance

Horizon Queen
Queue backend Redis the Queen broker: one binary, a replicated log
Durability in the benchmark Redis AOF, appendfsync always, everysec or no one Raft node, fsync on every acknowledged write
Jobs/s, 32 workers, 10 ms jobs, fsync on every write 1,124 2,753
Jobs/s, 64 workers 1,090 4,395
p95 from dispatch to completion, 500 jobs/s asked, 16 workers 227 ms 23 ms
Application memory, 64 workers 1,892 MiB 183 MiB, with forked workers
Master resident memory through a 45-minute soak 49.1 MiB 7.0 MiB Rust, 58.5 MiB PHP
Backend CPU per job, at 500 jobs/s and under load with 32 workers 0.49 and 0.37 ms 0.87 and 0.23 ms
A job that runs longer than retry_after can run in two workers at once lease renewal and fencing keep one attempt at a time
The jobs behind a long job free workers take them the jobs in its stripe wait for it
Order per entity none QueenPartitionable
Reaction to a burst reads queue sizes every balanceCooldown with event_driven, 20 workers 5.6 s after a burst, against 13.9 s when polling
Several masters on one queue each sizes its pools from the whole backlog opt-in coordination shares one target
Dashboard and alerts mature: job lists, batches, silenced jobs, Slack and SMS preview: process health, job metrics, tags, what each queue holds, failed jobs with a retry, tuning advice, mail, Prometheus
Clear a queue horizon:clear and queue:clear not supported: queue:clear fails and the jobs still run
15 failures on one Linux server all but one passed: 2 of 300 jobs ran twice after a Redis restart all passed, on both engines
32 Laravel queue features all passed all passed, on both engines
Maturity established preview, supervisors on Unix only

The measured rows come from the Laravel benchmark: one 16-vCPU Linux server and one Raft node of 2.0.0-beta.2 and beta.3 (2026-10-01 and 2026-10-02), with the reaction time from the supervisor runs of 2026-09-30. They are diagnostic, with a protocol written by the Queen team; the limits say what they do not show.

Choose Horizon when

  • Redis is the queue backend you want, and your team already runs it well.
  • You need silenced jobs, job lists, batches in the dashboard, or Slack and SMS alerts out of the box.
  • Several masters must run without a broker to coordinate through.
  • A native release artifact, or a supervisor that runs on Unix only, is not acceptable.
  • Jobs of very different lengths must share one queue, and a short job must never wait behind a long one.
  • The application has no use for Queen’s partitions, replay, timers or transactions.
  • The load is low and steady and backend CPU matters more than latency: there the Raft broker spends more CPU per job than Redis.

Evaluate Queen when

  • Queen is already, or should be, the queue backend.
  • The memory of the master or the workers matters, or workers run as Kubernetes pods that should follow the backlog.
  • Bursts must get workers within seconds, not within several cooldowns.
  • Jobs are short and you want each worker to complete more of them at the same durability, and keep growing with the worker count: on the benchmark server Horizon stayed near 1,100 jobs/s from 32 to 64 workers, while Queen went from 2,753 to 4,395.
  • A long job must never run twice at the same time, however long it takes.
  • Per-entity order, consumer groups, replay, or an ACK and a push committed together help the workload.
  • Your team can qualify its own jobs under at-least-once delivery and the current preview.

If Queen fits, follow the migration. If a Horizon-only feature above is a hard requirement, stay on Horizon until Queen has a tested replacement; rebuilding it in application code by accident is the worst of both.

Where Queen is ahead

More jobs per worker at the same durability

On the Linux server, 32 Queen workers on a Raft node that fsyncs every acknowledged write completed 2,753 jobs/s of 10 ms jobs, against 1,124 for Horizon on Redis with appendfsync always. Relaxing Redis did not close the gap: 1,247 with everysec and 1,313 with appendfsync no, while the Raft node kept fsyncing. With 64 workers Horizon stayed near 1,100 and Queen reached 4,395. Each Horizon job cost about 33 Redis commands and each Queen job 1.3 broker requests. Every Queen pop and ACK waits for an fsync, so the driver keeps those waits off the worker’s path: the ACK of a job travels while the next job runs (ack_async), and the pop for the next batch while the last job of the current one runs (pop_ahead).

Lower latency at a steady rate

With jobs dispatched one at a time at 500 jobs/s and 16 workers, 95% of the jobs completed within 227 ms of their dispatch on Horizon and within 23 ms on Queen. Over a 15-minute soak at 400 jobs/s the p95 was 249 against 22 ms. Horizon’s producer could not send 500 jobs/s, because each dispatch also writes Horizon’s monitoring records. On Docker Desktop with 8 workers the two were equal at 100 jobs/s, and with CPU-bound jobs of about 20 ms, where the job itself dominates.

Long jobs without a long retry_after

Laravel’s Redis queue gives a job back retry_after seconds after a worker took it, even while that worker still runs it, so a Horizon job has to end before retry_after. With lease renewal, Queen renews the lease of a running job and stops the worker before the lease ends if it cannot renew. A job with a longer $timeout of its own can then run past retry_after with no second attempt beside it, and retry_after only sets how soon the job a crashed worker was running runs again. In a fault run, a prefetched job ended 32.1 s after its push, past its 30 s lease, on its first attempt.

Smaller workers, and a smaller master

Horizon, like Laravel’s own queue:work, boots the framework once per worker. Prefork boots it once per master and forks every worker from it, so the workers share the framework and the opcache copy-on-write. On the Linux server, with the master renewing the leases instead of a helper per worker, 64 workers used 183 MiB against 1,892 MiB for Horizon: a Horizon worker held 28 MiB of private memory, a forked Queen worker 1.4 MiB. The Horizon workers ran with PHP’s default opcache.enable_cli=0 and the Queen workers with it on. The benchmark job is tiny, so a real application’s workers keep more private memory and the ratio will be smaller. The Rust master does not keep Laravel resident at all, and held 7.0 MiB through the soak against 49.1 MiB for Horizon’s.

Faster reaction to bursts

Horizon reads its queue sizes every balanceCooldown, three seconds by default, and moves balanceMaxShift workers per check; a Queen supervisor polls at the same pace by default. With event_driven, the broker wakes it when jobs arrive, and with fast_scale_up each step closes half the gap to the target, once a second while the pool climbs. On the Linux server a burst of 30,000 jobs, autoscaling from 1 to 64 workers, was done after 9.5 s with Queen, which reached 64 workers after 8.2 s, and after 37.0 s with Horizon, whose producer sent jobs too slowly for it to need more than 34.

Built for Kubernetes replicas

Coordinated replicas split every pool’s target through the broker’s key/value store, so a supervisor Deployment scales with its replica count. The Prometheus endpoint gives KEDA the backlog to scale on, and the dashboard lists every pod and warns about masters that share a queue without coordinating. Scale on Kubernetes puts the pieces together.

Metrics without a snapshot command

The workers record per-class metrics, monitored tags and what the long-wait alerts need into the broker, so the numbers cover every host and need no scheduled snapshot, and a long-wait alert reads the age of the oldest waiting job instead of estimating it (monitoring).

The queue model underneath

Choosing Queen also chooses what is below the supervisor: one ordered partition per key, created by its first push; consumer groups and replay over one stored copy; delayed jobs held by broker timers; a dead-letter queue kept in step with Laravel’s failed jobs; an ACK and a push or a timer committed as one transaction; and a cluster of three nodes that keeps serving through the loss of one. These are advantages only when the application uses them. A team that already runs Redis well and needs none of it may value Horizon’s smaller topology more.

Where Horizon is ahead

Less backend CPU at a low, steady rate

At 500 jobs/s with one job per dispatch, the Raft broker used 0.87 ms of CPU per job against 0.49 for Redis, and 0.97 against 0.54 in the 15-minute soak. Each job is then its own Raft entry, and about half of the broker’s CPU goes to handing work between its threads. Under load the order reverses, 0.23 against 0.37 ms per job with 32 workers. The application side was cheaper with Queen on the Linux server, 0.91 against 1.83 ms per job under load, with the PHP client’s cURL transport; on Docker Desktop, with the older Guzzle transport, the Queen workers used more. When CPU is the scarce resource, measure your own jobs first.

No wait behind a long job

On Redis any free worker takes the next job, so a long job delays no other. On Queen a job waits until the job ahead of it in its stripe has ended: a ten-minute job holds back the jobs that hashed to its stripe for up to ten minutes, even while other workers are idle. Give long jobs a queue of their own (queues and stripes).

Maturity, and a richer dashboard

Horizon has an established installation path, a long operating history and one familiar Redis surface, and horizon:clear empties a queue. Queen’s Laravel supervisor is a preview, with more decisions to make about the broker, the state directory and the native binary, and no atomic clear yet. Horizon’s dashboard also still has silenced jobs, lists of recent, pending and completed jobs, batches, Slack and SMS routes and per-supervisor controls. Queen’s panel covers process health, safe controls for a whole master, per-class metrics over up to a day, monitored tags, failed jobs with a one-click retry, what each queue holds and a tuning page; job lists and dead-letter operations live in the Queen dashboard, and forgetting and pruning failed jobs stay with Laravel’s commands.

Several masters without a broker in between

Horizon keeps live master and supervisor records in its Redis repository. Queen’s lock excludes a second master on the same host only. With coordination, masters register in the broker and each runs a share of every pool; without it, a Queen deployment runs one master per application and consumer group, with no rolling-surge overlap.

Fewer release artifacts

Horizon installs as a Composer package and relies on an outer process monitor, and so does the Queen PHP engine. The Rust engine adds a native binary, a platform matrix, an installer and a release that has to match the Composer one. The installer is explicit and checks every step, but it is one more deployment step.

Feature by feature

Horizon Queen
Laravel jobs standard dispatch, middleware, retry, backoff, failures the same Laravel surface
Dynamic balancing auto, with size or time auto, with our own size and time
Per-queue minimum minProcesses, on every queue min_processes_per_queue
Reaction to new jobs queue sizes read every balanceCooldown the same polling, or event_driven: the broker wakes the master
Scale-up rate balanceMaxShift per cooldown the same, or fast_scale_up: half the gap per cycle
Fixed allocation simple simple
Strict queue priority balance=false balance=off
Long jobs retry_after must exceed the job lease renewal, with fencing before the lease ends
Master PHP with Laravel and Horizon loaded the PHP engine, or the Rust engine
Worker memory every worker boots Laravel the same, or prefork: workers share one booted Laravel
Crash loops restart behaviour of Horizon and the outer monitor capped backoff and a one-probe circuit in both engines, for short-lived crashes only
Several master hosts master records in Horizon’s Redis repository opt-in coordination through the broker, otherwise one master
Dashboard the mature Horizon dashboard the supervisor panel for every host or pod, plus the Queen dashboard
Job metrics per job and per queue, from horizon:snapshot live per class, one hour to one day, from every worker; a per-queue chart from the broker’s counters
Tags and silencing automatic and tags(), monitored tags, silenced jobs automatic and tags(), monitored tags; no silencing
Long-wait alerts mail, Slack, SMS; the wait is estimated queen:check-waits: the broker’s age of the oldest waiting job, an event and mail
Autoscaler metrics not built in a Prometheus endpoint for KEDA and HPAs
Per-supervisor controls pause, continue and status per supervisor controls for the whole local master
Failed jobs Horizon’s commands and dashboard Laravel’s commands, a dead-letter copy kept in step, and a detail page with a retry
Clear a queue horizon:clear and queue:clear not supported
Native binary none needed the Rust engine, installed explicitly at a pinned version

Matching names do not mean identical algorithms. You can configure the same limits, but depth, runtime estimates, cooldowns and process lifecycles come from different implementations.

When things fail

On the Linux server, the same Laravel application went through 15 failures on Horizon, the Queen PHP supervisor and the Queen Rust supervisor: jobs that throw, time out or run out of memory, killed workers and masters, deploys during short and long jobs, a backend that restarts, freezes or is down at dispatch, and two masters with one killed or both restarted in turn. A lane passed when every job completed once or failed once into failed_jobs, and no two attempts of one job ever overlapped. Both Queen engines passed every lane. Horizon passed all but one: after a Redis restart, 2 of 300 jobs ran twice, because the worker could not remove them from Redis’s reserved set while it was down. A 45-minute soak with a worker killed every 10 minutes and a deploy halfway ended every one of 13,501 jobs as its kind said on all three, with 398 failed-job rows for 398 permanent failures.

A fourth profile, the Rust engine with prefetch 4, ack_async and pop_ahead, passed every lane but one: a job that crashes its worker charged an attempt to the jobs prefetched with it. The lease renewer now hands those jobs back after a crash (PHP client 1.9.0), and the lane passed in a rerun on both engines. The first run of these lanes found three defects in the client and the supervisors, fixed before the release. The benchmark page has every lane, the defects and the data.

All 32 Laravel queue features we checked passed on the three engines and the fourth profile: delays and retries, uniqueness and rate limits, chains and batches, queued listeners, notifications and closures, encrypted jobs, afterCommit and the queue events, and the failed-job commands. Only queue:clear differs. Laravel’s Redis queue stores due times in whole seconds, so a Horizon retry can start up to a second early or late: for backoff [1, 3], Horizon’s retries came after 1.81 and 4.03 s, Queen’s after 1.02 and 3.05 s.

The limits of this evidence

These results are diagnostic. Each comes from one host and a single broker node, so the fsync cost of one virtual disk sets the absolute numbers and no replication round trip is in them. The lanes and their checks are ours, and nobody else has repeated them yet. Before we call the Laravel stack generally available we still want a 24-hour soak, network and storage faults, the same failure lanes on a three-node cluster, broader scaling and partition matrices, and verified native releases for every target.

They verify the at-least-once paths we tested. They do not verify exactly-once external effects: in all three systems a process can perform a side effect and die before its acknowledgement is durable, so jobs still need an idempotency key.

The broker results near a million messages a second on the benchmarks page are broker-native push and pop with Go loaders. They do not run Laravel jobs and do not belong in this comparison.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close