Skip to content

PHP client

Complete surface of the PHP client: constructor options with defaults, every builder method, the Admin methods that reach a route that exists, the rdkafka-style consumer, and the Laravel integration.

Updated View as Markdown

The Composer package is queen-mq/php-client, PSR-4 namespace Queen\. It requires PHP 8.3 or newer plus Guzzle 7 and ramsey/uuid. The client is synchronous: there is no promise or async variant. Laravel integration (service provider, facade, artisan command) ships in the same package and is optional. The source is prepared for the aligned release train, but Packagist has no tagged release yet; install it from the repository only if you intentionally accept that status.

use Queen\Queen;

$queen = new Queen('http://localhost:6632');

Constructor

new Queen(string|array $config = [])

A string is a single URL. An array with a numeric key 0 is a list of URLs. Anything else is a config array merged over Defaults::CLIENT_DEFAULTS; without url or urls it throws InvalidArgumentException('Must provide urls or url in configuration').

Key Default Effect
url none Single broker URL.
urls none Multiple brokers. More than one entry activates the load balancer.
timeoutMillis 30000 Per-request deadline.
retryAttempts 3 Attempts for 5xx and network failures.
retryDelayMillis 1000 First retry delay; doubles per attempt.
loadBalancingStrategy 'affinity' 'affinity', 'round-robin' or 'session'.
affinityHashRing 128 Virtual nodes per backend on the hash ring.
enableFailover true On a 5xx or network error, try the next backend.
healthRetryAfterMillis 5000 How long a backend stays marked unhealthy.
bearerToken null Sent as Authorization: Bearer <token>.
headers [] Extra headers on every request.
retry429 [] HTTP 429 backoff policy.

The load-balancing keys are inert with a single URL: the LoadBalancer is only constructed when urls holds more than one entry.

retry429

retry429 takes maxAttempts, baseMs and capMs, with the constants on Queen\Http\Retry429Policy (UNBOUNDED is 0), separate from retryAttempts; the defaults, the Retry-After contract and the jitter are in What the clients do with a 429. capMs has a hard 300,000ms ceiling so a single retry sleep remains operationally bounded.

Queen

Method Signature
queue queue(?string $name = null): QueueBuilder
admin admin(): Admin. Lazily created, one per client
transaction transaction(): TransactionBuilder
ack ack(array|string $message, bool|string $status = true, array $context = []): array
renew renew(string|array $messageOrLeaseId): array
flushAllBuffers flushAllBuffers(): void
getBufferStats getBufferStats(): array
deleteConsumerGroup deleteConsumerGroup(string $consumerGroup, bool $deleteMetadata = true): mixed
updateConsumerGroupTimestamp updateConsumerGroupTimestamp(string $consumerGroup, string $timestamp): mixed
close close(): void. Flushes buffers (best effort) and cleans up

ack

$message is a message array, a transaction-id string, or an array of messages. $status is truecompleted, falsefailed, or a status string passed through verbatim. $context accepts group and error.

partitionId is mandatory on every message array; without it the builder throws InvalidArgumentException. A leaseId is forwarded when present. Per-message statuses inside a batch work by tagging items with _status and _error.

One message posts to /api/v1/ack, more than one to /api/v1/ack/batch. Both respond with one result per ack in request order.

renew

Accepts a lease-id string, a message array, or an array of either. Posts to /api/v1/lease/{leaseId}/extend once per distinct lease id.

QueueBuilder

$queen->queue($name). Configuration methods return static; terminal methods are push(), pop(), consume(), getConsumer(), create(), delete(), dlq() and flushBuffer().

Addressing

Method Default Notes
partition(string $name) 'Default' The ordered lane. Anything else switches pop to the partition-scoped route.
namespace(string $name) null Grouping label at create time; also a pop filter.
task(string $name) null Second grouping label; same dual role.
group(string $name) null Consumer group. Absent, the broker uses __QUEUE_MODE__.

Queue lifecycle

Method Wire call Returns
config(array $options) none static. Merged over QUEUE_DEFAULTS.
create() POST /api/v1/configure OperationBuilder
delete() DELETE /api/v1/resources/queues/:queue OperationBuilder; throws RuntimeException with no queue name

OperationBuilder exposes onSuccess(Closure), onError(Closure) and execute(): mixed.

Defaults::QUEUE_DEFAULTS carries nine keys: leaseTime, retryLimit, priority, delayedProcessing, windowBuffer, maxSize, retentionSeconds, completedRetentionSeconds and encryptionEnabled, with the values listed under Queue option defaults.

Extra keys you pass to config() are sent verbatim, so any option /configure accepts is reachable.

Since 1.6.0 /configure merges: an option the body does not carry keeps the value the queue already has, and only "mode": "replace" resets what the body omits. This client sends no mode, so create() always merges. Because config() merges over QUEUE_DEFAULTS, those nine keys are on the wire whether you named them or not and land on the client’s default rather than on the queue’s stored value; the twelve options outside that array, dedupWindowSeconds and retentionEnabled among them, are the ones a partial call now leaves alone.

Producing

Method Notes
buffer(array $options) messageCount / timeMillis / maxSize / retryDelayMillis / maxWaitMillis, defaults 100 / 1000 / 4 * messageCount / 250 / 5000. Turns push() into a bounded enqueue.
push(array $payload): PushBuilder One item array or a list of them. Throws RuntimeException with no queue name.

Each item may carry data, payload, or be the payload itself. The client mints a UUIDv7 transactionId via Queen\Support\Uuid::v7() when the item has none.

PushBuilder exposes onSuccess(Closure), onError(Closure), onDuplicate(Closure) and execute(): mixed. execute() returns the broker’s per-item array for a direct push; a buffered push returns without sending once the buffer accepts the message. At maxSize waiting messages the add flushes inline and keeps retrying under a maxWaitMillis deadline; if the deadline expires it throws RuntimeException and the message was not accepted. PHP has no background flusher to wait on, which is why the bound is a deadline here rather than an open-ended park.

$queen->queue('orders')
    ->partition('customer-42')
    ->push([['data' => ['orderId' => 8891]]])
    ->execute();

Consuming

Method pop() default consume() default Notes
batch(int $size) broker-sized broker-sized Total message budget for the call, shared across every partition it claims. Left unset the broker sizes it (pop autopilot); the pre-1.2 client default of 1 comes back with autopilot off. batch(0) reads as unset.
partitions(int $n) broker-sized broker-sized Claim up to N partitions per call; all share one leaseId. Left unset the broker sizes it (pop autopilot). Sent verbatim when set, 1 included, because a pinned width is a decision the broker must not widen; with autopilot off it is sent only when > 1.
autopilot(bool $enabled = true) true true Broker-side pop sizing for the knobs above that you did not set. On by default; false restores the pre-1.2 client defaults (batch 1, partitions 1) and sends no autopilot parameter. QUEEN_SDK_POP_AUTOPILOT=off does the same for a whole process, read once when the client is built. Setting both knobs leaves nothing to decide, so no parameter is sent then either. See pop autopilot. Requires broker >= 1.2; an older one applies its own defaults (batch 200, partitions 1) to the omitted knobs, which is a sizing difference and nothing more.
wait(bool $enabled) false true Long-poll.
timeoutMillis(int $millis) 30000 30000 Server-side deadline; the HTTP call adds 5 s of slack.
leaseSeconds(int $seconds) queue default queue default Per-pop lease override, clamped to at least one second. Keep framework worker timeouts shorter than this value.
autoAck(bool $enabled) false true The two meanings differ. See the warning.
subscriptionMode(string $mode) null null all | new.
subscriptionFrom(string $from) null null 'now' or an ISO timestamp.
conflation(bool $enabled = true) false false Last-value delivery: the pop returns only each partition’s newest visible message and retires the rest. Sent only when true. A property of the consumer GROUP, stored on its first registration; see conflation. Requires broker >= 1.1.0; against an older one the call raises rather than draining the backlog quietly.
concurrency(int $count) n/a 1 consume() only.
limit(int $count) n/a null consume() only.
idleMillis(int $millis) n/a null consume() only.
renewLease(bool $enabled, ?int $intervalMillis = null) n/a false consume() only.
each() n/a batch mode consume() only. One message per handler call.

pop(): array

Returns a list of message arrays, [] when there is nothing to return. Route selection: queue plus non-Default partition uses /api/v1/pop/queue/:queue/partition/:partition; queue alone uses /api/v1/pop/queue/:queue; namespace or task alone uses /api/v1/pop.

popResult(): array

Returns ['messages' => array, 'autopilot' => array|null]. It is the same call as pop() and returns the messages plus the broker’s account of how it sized the claim: partitions, batch, and an optional waitMillis pacing hint the consume loop honours in place of its own delay between empty polls. autopilot is null when the pop did not engage autopilot, when the broker is older than 1.2, or when the answer was a bodiless 204, which has no body to carry an echo. See pop autopilot.

consume(Closure $handler): ConsumeBuilder

ConsumeBuilder exposes onSuccess(Closure), onError(Closure) and execute(): void.

getConsumer(): HighLevelConsumer

A pull-loop consumer modelled on php-rdkafka’s KafkaConsumer, for code that wants to own its own loop instead of handing over a closure.

Method Notes
subscribe(): void Resolves the pop path and query from the builder’s options. Must be called before consuming.
consume(int $timeoutMs = 1000): ?array One message or null. Forces batch=1, wait=true and timeout=$timeoutMs.
consumeBatch(int $timeoutMs = 1000, int $maxMessages = 10): array Up to $maxMessages; [] when none.
ack(array $message, bool $success = true): array
nack(array $message): array
renewLease(array|string $messageOrLeaseId): array
isClosed(): bool
close(): void

consume() and consumeBatch() call pcntl_signal_dispatch() when the extension is available, so a SIGTERM handler you installed runs between polls. Long-poll timeouts and connection refusals are absorbed into null / []; every other exception propagates.

$message['trace']

Messages returned by HighLevelConsumer carry a trace closure:

$message['trace'](['traceName' => 'tenant-acme', 'eventType' => 'info', 'data' => ['step' => 1]]);

data is required; traceName accepts a string or an array of strings; eventType defaults to 'info'. It never throws: failures return ['success' => false, 'error' => ...].

Buffering

flushBuffer(): void flushes this builder’s queue/partition buffer; throws RuntimeException with no queue name. Buffers are keyed on "<queue>/<partition>", so builders addressing the same pair share one. A buffer flushes at messageCount messages or timeMillis after its first message.

The buffer is bounded and lossless under errors. A batch whose POST fails is restored to the front of the buffer, in order, and retried after retryDelayMillis; it is never dropped. At maxSize waiting messages an add flushes inline under the maxWaitMillis deadline and throws if it expires, so a broker outage surfaces as an exception with everything still queued, not as silent loss.

Dead-letter queue

dlq(?string $consumerGroup = null): DLQBuilder. Throws RuntimeException with no queue name.

Method Notes
limit(int $count) / offset(int $count) Paging.
from(string $timestamp) / to(string $timestamp) Time filters.
get(): array ['messages' => [...], 'total' => n]

Read-only. Replay lives on the admin façade: retryMessage(string $partitionId, string $transactionId).

Since broker 1.6.0 that route runs on a move: one transaction claims the newest dead-letter row at the address, pushes it back into the log and deletes the row, and the replayed message carries the deterministic transaction id dlq:<dead-letter row id>. A second call for the same address answers 404 rather than replaying a second copy, only the addressed consumer group’s record is removed, and a result: "duplicate" verdict means nothing was written and nothing was removed. See Messages and dead letters.

TransactionBuilder

$queen->transaction(). Pushes and acks in one PostgreSQL transaction, all-or-nothing.

Method Notes
ack(array|object $messages, string $status = 'completed', array $context = []): static Requires transactionId and partitionId; a leaseId is collected into requiredLeases.
queue(string $queueName): TransactionQueueBuilder Sub-builder with partition(string) and push(array): TransactionBuilder.
addPushOperation(string $queueName, ?string $partition, array $items): void Lower-level entry used by the sub-builder.
commit(): array POST /api/v1/transaction.
$queen->transaction()
    ->ack($message)
    ->queue('orders.enriched')->push([['data' => ['id' => 1]]])
    ->commit();

A transaction is atomic, not exactly-once end to end; see the limit.

Admin

$queen->admin(). Every method returns the broker’s decoded body. Methods taking array $params turn it into a query string.

Resources

Method Route
getOverview() GET /api/v1/resources/overview
getNamespaces() GET /api/v1/resources/namespaces
getTasks() GET /api/v1/resources/tasks
listQueues(array $params = []) GET /api/v1/resources/queues
getQueue(string $name) GET /api/v1/resources/queues/:name
getQueueDepth(string $name, ?string $group = null, ?int $timeoutMillis = null) GET /api/v1/resources/queues/:name/depth
getQueueDepthAsync(string $name, ?string $group = null, ?int $timeoutMillis = null) Same route through the failover-aware async client.

Depth is the lightweight scaling read: total pending, live-lease processing, claimable ready, their partition counts, and effectivePending / effectiveReady adjusted for conflation. Per-partition rows carry pending, processing and ready. It has no message timestamps or segment scan; older brokers may omit the lease-aware fields during a rolling upgrade.

Messages and traces

Method Route
listMessages(array $params = []) GET /api/v1/messages
getMessage(string $partitionId, string $transactionId) GET /api/v1/messages/:partitionId/:transactionId
deleteMessage(string $partitionId, string $transactionId) DELETE /api/v1/messages/:partitionId/:transactionId
retryMessage(string $partitionId, string $transactionId) POST /api/v1/messages/:partitionId/:transactionId/retry
getTraceNames(array $params = []) GET /api/v1/traces/names
getTracesByName(string $traceName, array $params = []) GET /api/v1/traces/by-name/:traceName
getTracesForMessage(string $partitionId, string $transactionId) GET /api/v1/traces/:partitionId/:transactionId

Status and analytics

Method Route
getStatus(array $params = []) GET /api/v1/status
getQueueStats(array $params = []) GET /api/v1/status/queues
getQueueDetail(string $name, array $params = []) GET /api/v1/status/queues/:name
getAnalytics(array $params = []) GET /api/v1/status/analytics
getSystemMetrics(array $params = []) GET /api/v1/analytics/system-metrics
getWorkerMetrics(array $params = []) GET /api/v1/analytics/worker-metrics
getPostgresStats() GET /api/v1/analytics/postgres-stats

Consumer groups

Method Route
listConsumerGroups() GET /api/v1/consumer-groups
getConsumerGroup(string $name) GET /api/v1/consumer-groups/:name
getLaggingConsumers(int $minLagSeconds = 60) GET /api/v1/consumer-groups/lagging?minLagSeconds=
deleteConsumerGroupForQueue(string $consumerGroup, string $queueName, bool $deleteMetadata = true) DELETE /api/v1/consumer-groups/:group/queues/:queue
seekConsumerGroup(string $consumerGroup, string $queueName, array $options = []) POST /api/v1/consumer-groups/:group/queues/:queue/seek
refreshConsumerStats() POST /api/v1/stats/refresh

seekConsumerGroup posts $options verbatim: ['toEnd' => true] or ['timestamp' => '<RFC3339>'].

System

Method Route
health() GET /health
metrics() GET /metrics
getMaintenanceMode() GET /api/v1/system/maintenance
setMaintenanceMode(bool $enabled) POST /api/v1/system/maintenance
getPopMaintenanceMode() GET /api/v1/system/maintenance/pop
setPopMaintenanceMode(bool $enabled) POST /api/v1/system/maintenance/pop

Errors

Non-2xx responses throw Queen\Exceptions\HttpException, with isClusterSuspended() and isRateLimited() helpers. The machine-readable codes are constants on Queen\Exceptions\ErrorCode, one per proxy code listed under Behind the proxy.

A 204 response yields null: a 204 carries no body at all.

Laravel integration

The package auto-registers Queen\Laravel\QueenServiceProvider and the Queen facade alias. Publish the config with the queen-config tag; it lands at config/queen.php. The provider binds Queen\Queen as a singleton and aliases it to queen.

Config keys and env vars

Config key Env var Default
url QUEEN_URL http://localhost:6632
urls QUEEN_URLS unset (comma-separated list, wins over url)
bearer_token QUEEN_BEARER_TOKEN unset
timeout QUEEN_TIMEOUT 30000
retry_attempts QUEEN_RETRY_ATTEMPTS 3
retry_delay QUEEN_RETRY_DELAY 1000
load_balancing_strategy QUEEN_LB_STRATEGY affinity
enable_failover QUEEN_ENABLE_FAILOVER true
affinity_hash_ring QUEEN_AFFINITY_HASH_RING 150
health_retry_after QUEEN_HEALTH_RETRY_AFTER 30000
headers none []
queue QUEEN_QUEUE default
consumer_group QUEEN_CONSUMER_GROUP laravel
partitions QUEEN_PARTITIONS 64 (clamped to the broker maximum)
partition_prefix QUEEN_PARTITION_PREFIX laravel
retry_after QUEEN_RETRY_AFTER 90 seconds
block_for QUEEN_BLOCK_FOR 0 seconds
prefetch QUEEN_PREFETCH 1 job
ack_batch QUEEN_ACK_BATCH 1 job (cannot exceed prefetch)
lease_renewal QUEEN_LEASE_RENEWAL false
lease_renewal_interval QUEEN_LEASE_RENEWAL_INTERVAL derived from lease timing
lease_renewal_timeout QUEEN_LEASE_RENEWAL_TIMEOUT 5 seconds
lease_renewal_kill_grace QUEEN_LEASE_RENEWAL_KILL_GRACE 2 seconds
lease_renewal_safety_margin QUEEN_LEASE_RENEWAL_SAFETY_MARGIN 1 second
bulk_batch QUEEN_BULK_BATCH 100 jobs
after_commit QUEEN_AFTER_COMMIT false
sync_failed_jobs QUEEN_SYNC_FAILED_JOBS true
failed_jobs_lock_store QUEEN_FAILED_JOBS_LOCK_STORE Laravel default cache store
failed_jobs_lock_name QUEEN_FAILED_JOBS_LOCK_NAME queen:failed-jobs
failed_jobs_lock_ttl QUEEN_FAILED_JOBS_LOCK_TTL 600 seconds
failed_jobs_lock_wait QUEEN_FAILED_JOBS_LOCK_WAIT 600 seconds
supervisor.poll_interval QUEEN_SUPERVISOR_POLL_INTERVAL 3 seconds
supervisor.http_timeout QUEEN_SUPERVISOR_HTTP_TIMEOUT 5 seconds
supervisor.control_ttl QUEEN_SUPERVISOR_CONTROL_TTL 3600 seconds
supervisor.heartbeat_timeout QUEEN_SUPERVISOR_HEARTBEAT_TIMEOUT derived above the bounded loop budget
supervisor.read_bearer_token QUEEN_SUPERVISOR_READ_BEARER_TOKEN unset; falls back to bearer_token
supervisor.shutdown_grace QUEEN_SUPERVISOR_SHUTDOWN_GRACE 75 seconds
supervisor.process_limit QUEEN_SUPERVISOR_PROCESS_LIMIT 256
supervisor.state_directory QUEEN_SUPERVISOR_STATE_DIRECTORY storage/queen-supervisor
supervisor.telemetry_ttl QUEEN_SUPERVISOR_TELEMETRY_TTL 300 seconds
dashboard.enabled QUEEN_DASHBOARD_ENABLED false
dashboard.path QUEEN_DASHBOARD_PATH queen
dashboard.domain QUEEN_DASHBOARD_DOMAIN unset
dashboard.middleware none ['web']
dashboard.refresh_seconds QUEEN_DASHBOARD_REFRESH_SECONDS 5 seconds
dashboard.allow_local QUEEN_DASHBOARD_ALLOW_LOCAL true
dashboard.failed_jobs_limit QUEEN_DASHBOARD_FAILED_JOBS_LIMIT 50 rows
supervisor_binary.install_path QUEEN_SUPERVISOR_INSTALL_PATH storage/queen-supervisor-bin
supervisor_binary.release_base_url QUEEN_SUPERVISOR_RELEASE_BASE_URL unset
supervisor_binary.manifest QUEEN_SUPERVISOR_MANIFEST unset
supervisor_binary.manifest_sha256 QUEEN_SUPERVISOR_MANIFEST_SHA256 unset
retry_429.maxAttempts QUEEN_RETRY_429_MAX_ATTEMPTS unset
retry_429.baseMs QUEEN_RETRY_429_BASE_MS unset
retry_429.capMs QUEEN_RETRY_429_CAP_MS unset

Facade

use Queen\Laravel\QueenFacade as Queen;

Queen::queue('orders')->push([['data' => ['id' => 1]]])->execute();

The facade proxies queue(), transaction(), admin(), ack(), renew(), flushAllBuffers(), getBufferStats() and close().

Laravel Queue driver

The provider also registers a queen Laravel queue connection. Existing jobs can use dispatch(), middleware, backoff and failed-job events unchanged:

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

retry_after becomes the Queen lease duration and must exceed the worker/job timeout. block_for is the long-poll duration; leave it at 0 for a worker that scans a comma-separated priority queue list. Zero disables broker waiting but retains the normal 30-second request budget; positive values become the broker timeout and the HTTP client adds five seconds of transport slack.

The default prefetch=1 and ack_batch=1 keep the per-job synchronous ACK boundary. Short, idempotent workloads can opt into a larger prefetch; the connector then requires lease_renewal=true on every worker path. One pop feeds the standard sequential Laravel worker, while the helper keeps the active job, local tail and deferred acknowledgements leased. This remains at-least-once delivery and widens the crash duplicate window. An exact-PID SIGCHLD watchdog fences the worker if its helper disappears while a lease is active. The renewed profile requires Unix CLI PHP with PCNTL/POSIX support; application code must not replace Queen’s signal handler. Leave prefetch at 1 for unsupported platforms, strict per-job confirmation or comma-separated priority queues. A reentrant pop() before the active prefetched job is deleted or released fails explicitly.

Supervised workers validate timeout < retry_after; the driver also rejects an unsafe explicit per-job timeout when renewal is off. Laravel does not expose a direct queue:work --timeout value to the connection, so an external process manager must enforce that inequality itself.

Laravel Queue::bulk() is overridden with bounded multi-partition Queen push requests. bulk_batch limits each request; ordinary dispatch() remains a single-job operation.

Ordinary jobs are assigned deterministically to a bounded set of partition stripes. A job implementing Queen\Laravel\Contracts\QueenPartitionable::queenPartition() supplies its own ordering key instead. Delayed dispatch and positive backoff use Queen timers; an immediate release uses an atomic ack-and-push transaction. A positive backoff uses an atomic ack-and-timer transaction, and a final Laravel failure is acked to Queen’s DLQ. Laravel’s failed-job provider is decorated by default so retry, forget, flush and prune also remove the matching Queen snapshot. Failed payloads carry a stable retry transaction ID; queue:retry resets their attempt count and can safely bypass the original dispatch’s deduplication record. Use Laravel’s command for Laravel jobs, not generic Admin retryMessage(), because only Laravel owns the failed_jobs index.

Laravel Queue::size() obtains delayed work with the broker’s indexed count(queue, 'laravel:') timer operation, so it does not download keyset pages. The client falls back to legacy paging when the broker explicitly returns no_such_route/unsupported, or when the immediately preceding broker ignores mode=count and returns the exact, well-formed list-page contract. That page is reused as page one. Timeouts, 5xx responses, malformed pages, missing cursors and cyclic cursors remain visible errors instead of producing a plausible but partial size.

The driver can still run under any ordinary process manager. For Horizon-like local pools, the package also provides the PHP and Rust supervisors below; the Queen broker itself never spawns PHP workers.

The broker emits a group-scoped deliveryAttempt for every popped message. It increments after nack, worker crash or lease expiry; the driver combines it with explicit releases for Laravel attempts() and --tries. During a rolling upgrade an older broker’s missing field falls back to 1, so upgrade brokers before depending on crash-attempt enforcement.

Laravel monitoring reads the worker consumer group’s cheap depth endpoint. size() is total pending (including live leases) plus Laravel-owned timers, pendingSize() is claimable ready, and reservedSize() is live-lease processing. Missing lease-aware fields retain the previous counters during a rolling broker upgrade.

For oldest-job age, ready: 0 returns null without the segment-backed queue detail call. With ready work the depth response has no timestamps, so the driver filters queue detail to ready partitions and returns a conservative approximation. Queue detail uses the queue-wide worst cursor; its oldestMessage can therefore predate this group’s first unleased job. Older brokers use the same approximate fallback.

Worker supervisors

The guided configuration, platform matrix and production checks are in Worker supervisors. The PHP engine keeps one Laravel master resident:

php artisan queen:supervise

The separate Rust binary starts the same queue:work children after one temporary Artisan invocation resolves the versioned configuration. Install the package-pinned native asset explicitly, then use the Composer launcher:

php artisan queen:supervisor-install
vendor/bin/queen-supervisor --php php --artisan artisan
php artisan queen:supervisor-config --pretty

The online install succeeds only when a manifest and asset exist for the package-pinned supervisor/v* release. Preview packages do not guarantee published assets, so use the PHP engine or a verified local manifest/archive pair until the matching release is available. The immediate parent of supervisor_binary.install_path must be an existing real directory; the install leaf must be application-owned and must not be group- or world-writable. See Run the Rust engine for the secure path example and platform status.

The normal exporter redacts bearer tokens and header values. Pass --for-engine only when producing input for a supervisor engine; that form contains the credentials.

Each entry in supervisor.supervisors carries connection, consumer_group, an ordered queues list, Laravel worker options and its scaling policy.

Setting Meaning
balance=auto Dynamic target, distributed across queues by pressure.
balance=simple Fixed processes, spread evenly.
balance=off Every worker receives the ordered comma-separated queue list.
strategy=size ceil(sum(effectivePending) / target_jobs_per_process).
strategy=time ceil(sum(effectivePending × runtime) / target_clear_seconds).
min_processes / max_processes Lower and upper target bounds.
balance_cooldown / balance_max_shift Reconciliation cadence and per-cycle change bound.
scale_down_delay How long a lower target must persist before downscaling.
restart_backoff / restart_backoff_max Bounds repeated worker restart attempts.
stable_after Runtime after which the crash backoff resets.
default_runtime_seconds Time-strategy fallback before worker telemetry exists.

Both engines use capped exponential restart backoff. Only the Rust engine opens a circuit after five consecutive crashes and permits one probe after the cooldown; the PHP engine has no open/probe phase and continues at restart_backoff_max.

The local control command is shared by both engines:

php artisan queen:supervisor status [--json]
php artisan queen:supervisor status --check # non-zero unless live
php artisan queen:supervisor pause
php artisan queen:supervisor continue
php artisan queen:supervisor terminate

Status, commands, the exclusive PID lock and duration telemetry live below state_directory. Pause/continue map to SIGUSR2/SIGCONT; termination sends SIGTERM and enforces shutdown_grace. This surface targets Unix and requires PHP pcntl.

QUEEN_URLS supplies failover broker endpoints for depth reads. Both v2 engines read the selected pool’s resolved Queen connections entry directly and build an isolated client from its endpoints, bearer token and validated headers; neither reuses a worker’s live Laravel client. QUEEN_SUPERVISOR_READ_BEARER_TOKEN may replace the worker token with a read-only one while retaining the other custom headers. Both engines poll in waves of at most 16 concurrent depth requests and fail over network/5xx errors. The exported JSON contains the read token and headers only with --for-engine: do not log that form, prefer piping it directly to the Rust process, and protect any credential-bearing --config file with mode 0600.

Supervisor dashboard

The package also provides a disabled-by-default local panel. It reads bounded state from the one local master and protects HTML, JSON and controls with the same viewQueenDashboard Gate. In production, enabling the route without defining an allowing Gate still returns 403.

The complete authorization example, route-cache procedure and local-versus-global scope are in Supervisor dashboard.

Artisan command

Registered only when running in console.

php artisan queen:consume orders "App\\Handlers\\OrderHandler"
Argument / option Notes
queue Queue name to consume from.
handler Fully qualified class name with a handle() method.
--group= Consumer group.
--batch=1 Messages per batch.
--auto-ack Enable auto-acknowledgment.
--subscription-mode= Subscription mode.
--subscription-from= Subscription start point.
--timeout=30000 Long-poll timeout in milliseconds.
--idle-timeout= Stop after N milliseconds of inactivity.
--limit= Stop after processing N messages.

The command errors out when the handler class does not exist.

Not in this client

There is no streaming SDK for PHP. The Stream builder, windows, gates and the /streams/v1/* runtime exist in the JavaScript, Python, Go and Rust clients.

Same surface, other languages

Parity here is a claim about the surface, not about shared code: each client is written natively in its own idiom. These sixteen rows are that surface, read out of clients/ on this tree. Six of them are full parity across all six clients; the other ten are where a client stops.

Capability JavaScript Python Go Rust PHP C++
Push, pop, ack, multi-partition claim yes yes yes yes yes yes
Client-side push buffering yes yes yes yes yes yes
Transactions through POST /api/v1/transaction yes yes yes yes yes yes
Dead-letter reader yes yes yes yes yes yes
HTTP 429 policy, separate from the retry counter yes yes yes yes yes yes
kv and timers riders on a transaction yes yes yes yes yes yes
Key/value: the seven operations yes yes yes yes yes five of seven
Timers: schedule, cancel, peek, list yes yes yes yes yes two of four
once, the idempotency marker in one call yes yes no no no no
Admin facade yes yes yes yes yes no
affinity load-balancing strategy yes yes yes yes yes no
Streams DSL over /streams/v1/* yes yes yes yes no no
Lease renewal inside the consume loop yes yes yes yes yes no
Per-message trace helper yes yes yes no yes no
Host override independent of the address dialled yes no no yes no no
pop reports a failure instead of an empty result no no yes yes yes no

The ten divergent rows, with their conditions:

  • Key/value. The seven operations are get, getMany, getPrefix, put, putIfAbsent, delete and incr. C++ wraps five of them: getMany and getPrefix have no method, and both remain reachable through POST /api/v1/kv on the shared HttpClient. getPrefix inside a transaction is refused by the broker in every client, which is its rule and not a client’s gap.
  • Timers. C++ has schedule and cancel only; peek and list are reads and stay on GET /api/v1/timers/{queue}[/{timerKey}]. There is no reschedule operation anywhere, because schedule is the upsert and status reports which happened; Python, Go, Rust and PHP still spell a reschedule alias over it, JavaScript and C++ do not.
  • once. JavaScript (kv.once, transaction().once) and Python (kv.once, transaction().once) fold putIfAbsent plus required into the question people actually ask, “did I win?”. The other four write the putIfAbsent themselves, which is the same wire and one line longer.
  • kv on a streams operator context. No client has it, so it is not a row. Inside a stream the state primitive is state_ops, which commits with the sink and the ack in the cycle’s own transaction; a key/value write from an operator would not, and that atomicity is the thing the stream already gives you for free.
  • Admin facade. C++ has no Admin class. Every management and observability route is still reachable, through the shared HttpClient returned by get_http_client().
  • affinity. The C++ LoadBalancer implements round-robin and "session" only, and load_balancing_strategy = "affinity" falls through to round-robin silently. The strategy matters because it keeps one consumer’s pops on one backend, which is what keeps two clients from contending on the same partition claim.
  • Streams. The Stream builder, the four window kinds, gates and the /streams/v1/* runtime are in the JavaScript, Python, Go and Rust clients. PHP and C++ have none of it.
  • Lease renewal. In C++, renew_lease() sets a flag the consumer loop does not act on: the worker carries an unimplemented placeholder where the timer would go. Call client.renew(message) yourself before the lease expires.
  • Per-message trace. Where it exists it is attached by the consume loop, so a message returned by a manual pop() never carries it. Rust records a trace through Admin::record_trace instead of hanging a method off the message. C++ cannot hang one off a JSON object at all, and its trace hook is an explicit no-op.
  • Host override. JavaScript (hostHeader) and Rust (host_header) advertise a request authority, and a TLS SNI, independent of the address the socket dials. That is what addresses a named tenant cluster behind a shared proxy endpoint. The other four clients send the dialled address.
  • pop failures. Go, Rust and PHP surface a 4xx, an exhausted 429 budget, a terminal 403 or a network fault to the caller, so an empty result means an empty queue. JavaScript, Python and C++ log the failure and return an empty result, which does not distinguish an empty queue from revoked credentials.

PHP is the one client with no suite in the test matrix: nothing in test/run.sh executes it. The rows above are read off clients/client-php/src, not off a run, and the package’s own tests/ directory is the only thing that exercises it.

Source of truth
Navigation

Type to search…

↑↓ navigate↵ selectEsc close