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
true → completed, false → failed, 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=3retry_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:superviseThe 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 --prettyThe 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 terminateStatus, 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,deleteandincr. C++ wraps five of them:getManyandgetPrefixhave no method, and both remain reachable throughPOST /api/v1/kvon the sharedHttpClient.getPrefixinside a transaction is refused by the broker in every client, which is its rule and not a client’s gap. - Timers. C++ has
scheduleandcancelonly;peekandlistare reads and stay onGET /api/v1/timers/{queue}[/{timerKey}]. There is no reschedule operation anywhere, becausescheduleis the upsert andstatusreports which happened; Python, Go, Rust and PHP still spell areschedulealias over it, JavaScript and C++ do not. once. JavaScript (kv.once,transaction().once) and Python (kv.once,transaction().once) foldputIfAbsentplusrequiredinto the question people actually ask, “did I win?”. The other four write theputIfAbsentthemselves, which is the same wire and one line longer.kvon a streams operator context. No client has it, so it is not a row. Inside a stream the state primitive isstate_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.Adminfacade. C++ has noAdminclass. Every management and observability route is still reachable, through the sharedHttpClientreturned byget_http_client().affinity. The C++LoadBalancerimplements round-robin and"session"only, andload_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
Streambuilder, 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. Callclient.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 manualpop()never carries it. Rust records a trace throughAdmin::record_traceinstead 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. Hostoverride. 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.popfailures. 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.