This page puts the supervisor features together for the setup most teams end up with: Laravel workers in a Kubernetes Deployment, several pods sharing the backlog of the same queues, and a replica count that follows that backlog. Inside each pod a supervisor fills its share of the target within seconds of a burst; across pods, KEDA or an autoscaler adds and removes pods over minutes. Every feature is optional and off by default, so start from the table and turn on what your workload needs.
| Situation | Feature | Switch |
|---|---|---|
| Several pods work the same queues and consumer group | Coordinated replicas share one worker target | QUEEN_SUPERVISOR_COORDINATION=true |
| Worker memory decides how many workers fit in a pod | Prefork: Laravel boots once per pod and every worker is forked from it | QUEEN_SUPERVISOR_PREFORK=true and opcache.enable_cli=1 |
| Short jobs, where the broker round trip dominates | Asynchronous ACKs and popping ahead | QUEEN_ACK_ASYNC=true and QUEEN_POP_AHEAD=true |
| Bursts must be served in seconds | Event-driven scaling with fast scale-up | QUEEN_SUPERVISOR_EVENT_DRIVEN=true and fast_scale_up |
| A quiet queue must not wait for a cold start | A per-queue minimum | min_processes_per_queue |
| Some queues need workers of their own | A simple pool beside an auto pool |
two entries in supervisors |
| The number of pods follows the backlog | The Prometheus endpoint, read by KEDA or an HPA | QUEEN_METRICS_ENABLED=true |
| A queue that waits too long must page someone | Long-wait alerts | queen:check-waits every minute |
Coordination, prefork, the per-queue minimum, fast scale-up and the Prometheus endpoint need PHP client 1.7.0; event-driven scaling 1.8.0; asynchronous ACKs, popping ahead and lease renewal in the master 1.9.0.
Configure the pools
One configuration serves every pod. This one keeps two workers on payments in every pod and moves
between 1 and 10 workers per pod across three other queues, by backlog:
// config/queen.php
'supervisor' => [
'shutdown_grace' => 75,
'process_limit' => 16,
'supervisors' => [
'payments' => [
'queues' => ['payments'],
'balance' => 'simple',
'processes' => 2,
],
'shared' => [
'queues' => ['emails', 'default', 'reports'],
'balance' => 'auto',
'strategy' => 'size',
'min_processes' => 1,
'max_processes' => 10,
'min_processes_per_queue' => 1,
'target_jobs_per_process' => 10,
'fast_scale_up' => true,
'timeout' => 60,
'retry_after' => 90,
],
],
],QUEEN_SUPERVISOR_COORDINATION=true
QUEEN_SUPERVISOR_PREFORK=true
QUEEN_SUPERVISOR_EVENT_DRIVEN=true
QUEEN_SUPERVISOR_STATE_DIRECTORY=/var/lib/queen-supervisor
QUEEN_SUPERVISOR_INSTALL_PATH=/opt/queen-supervisor-bin
QUEEN_SUPERVISOR_REMOTE_STATUS=true
QUEEN_SUPERVISOR_REMOTE_STATUS_KEY=orders-productionWith coordination, min_processes and max_processes apply to each pod: three pods run the
shared pool between 3 and 30 workers together, while payments runs 2 workers in each. If you
also turn on lease renewal (the faster profile needs it), every worker reserves two process slots,
one for its renewal helper, so these two pools need a process_limit of 24. Remote
status lets the dashboard and the Prometheus endpoint on the web pods see every worker pod, so set
the two REMOTE_STATUS variables on the web pods too. event_driven holds its long poll with the
token the supervisor reads depth with, and through the broker’s
embedded proxy that poll carries the authority of a pop: if the pods reach
the broker through the proxy with a read-only key, the supervisor logs it once and keeps polling.
Build the image
Install the native supervisor while the image builds, and create its state directory there. The
state directory must be private to the supervisor’s user, under parents only root can write to. A
Kubernetes emptyDir is world-writable without the sticky bit, so it can be neither the state
directory nor one of its parents.
# The command-line opcache is shared by the forked workers.
RUN echo "opcache.enable_cli=1" > "$PHP_INI_DIR/conf.d/opcache-cli.ini"
RUN install -d -m 0755 /opt/queen-supervisor-bin \
&& QUEEN_SUPERVISOR_INSTALL_PATH=/opt/queen-supervisor-bin php artisan queen:supervisor-install \
&& install -d -o www-data -g www-data -m 0700 /var/lib/queen-supervisor
USER www-datavendor/bin/queen-supervisor verifies the installed binary and then replaces itself with it, so
the Rust supervisor is the container’s main process and receives Kubernetes’ SIGTERM directly.
Deploy the worker pods
apiVersion: apps/v1
kind: Deployment
metadata:
name: queen-workers
spec:
replicas: 3
strategy:
type: RollingUpdate # safe with coordination: old and new pods share the target
selector:
matchLabels: { app: queen-workers }
template:
metadata:
labels: { app: queen-workers }
spec:
terminationGracePeriodSeconds: 90 # longer than shutdown_grace (75)
containers:
- name: supervisor
image: registry.example.com/orders:1.42.0
workingDir: /var/www/html
command: ["vendor/bin/queen-supervisor", "--php", "php", "--artisan", "artisan"]
envFrom:
- secretRef: { name: orders-env }
livenessProbe:
exec:
command: ["php", "artisan", "queen:supervisor", "status", "--check-liveness"]
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3On SIGTERM the supervisor stops starting workers and sends SIGTERM to each of them, so they finish
their current job. While they drain, for up to shutdown_grace seconds, it leaves the coordination
so that the other pods take its share at their next poll, and then it forces the rest down. Since
PHP client 1.9.0 the workers get their SIGTERM before any broker call, so a slow broker cannot delay
it. Keep
terminationGracePeriodSeconds above shutdown_grace, or Kubernetes kills jobs that were still
finishing. A pod that crashes still counts in the coordination until its key expires, so capacity
can be short by its share for up to one control-loop bound.
Each probe boots Laravel once, so keep its period in tens of seconds. status --check-liveness
asks only whether the master is alive. status --check also requires every pool to be ready, and
fits a readiness probe if anything routes traffic to the pod.
Without coordination the rule is one master per application and consumer group: one replica, no
autoscaler, and strategy: { type: Recreate }.
Scale the pods on the backlog
The web pods serve the Prometheus endpoint, GET /queen/metrics, with a bearer token of at least
32 characters. Point Prometheus at them, then let KEDA scale the worker Deployment on the depth the
supervisors report:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: queen-workers
spec:
scaleTargetRef:
name: queen-workers
minReplicaCount: 2
maxReplicaCount: 10
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: max(queen_queue_depth{consumer_group="laravel",queue=~"emails|default|reports"})
threshold: "100" # max_processes (10) x target_jobs_per_process (10)A new pod joins the coordination at its first poll and takes its share; a pod removed by a
scale-down leaves at once and drains. Inside the pods, event_driven and fast_scale_up fill each
pod’s share within seconds, while KEDA changes the number of pods over minutes.
Check that it works
kubectl exec deploy/queen-workers -- php artisan queen:supervisor status --json: inpool_status, each autoscaling pool reportsreplicas, the number of pods sharing its target.- The dashboard’s Supervisors page lists one card per pod, and each
sharedpool shows its share. A warning about masters that autoscale one queue means a pod runs without coordination. - In Prometheus,
sum(queen_workers)stays within the fleet limits andqueen_pool_replicasmatches the replica count. - The pod log says
event-driven: watching N partition(s). If watching is off, the line says why, such as a token that may not consume.
What to expect
Each row comes from the Laravel benchmark. The first three ran on Docker Desktop against a Queen 1.6.0 broker on PostgreSQL (2026-09-30), the fourth and fifth on Docker Desktop against a single Raft node, the last two on a 16-vCPU Linux server against a single Raft node (2026-10-01). Read them as the size of each effect, not a promise for your cluster.
| Feature | Without | With |
|---|---|---|
| Coordination, two pods of 8 on a draining backlog | up to twice the fleet target (12 workers for 6) | the target at every sample |
| Prefork and opcache, 8 workers after 600 jobs | 336 MiB | 85 MiB |
event_driven with fast_scale_up, time to 20 workers after a burst |
13.9 s | 5.6 s |
| Lease renewal in the master, 8 prefetching workers and their master | 161 MiB | 70 MiB |
ack_async and pop_ahead, 10 ms jobs, 8 workers |
453 jobs/s | 643 jobs/s |
| Lease renewal in the master, 32 workers | 417 MiB | 116 MiB |
ack_async and pop_ahead, 32 workers |
1,879 jobs/s | 2,794 jobs/s |
Next: the supervisor settings used here, and monitoring for job metrics, tags and long-wait alerts.