NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist
A powerful task queue.
Last release 13 days ago
25 Sep 2026
Ships fairly regularly
a new release about every 9 days
Some releases are documented
notes for 31 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
4 years old
97 releases · first in 2022
One column per quarter.
Merge pull request #350 from utopia-php/fix/queue-retry-publish-after-connection-loss
Three changes, two of them behavioural. Nothing in this release announces itself through the type system except the first, so read the second and thir
Three changes, two of them behavioural. Nothing in this release announces itself through the type system except the first, so read the second and third before upgrading.
Publisher\Synchronous::getFailedCount()New method on the interface. It reports everything a queue could not get through — on Redis the failed, dead and poison lists together, on NATS the dead stream.
getQueueSize() keeps its name and signature and delegates to it, so no caller has to move. Implementers do: any class implementing Publisher\Synchronous needs the new method.
Counting only the failed list answered zero through exactly the incidents the other lists exist to record — a handler declaring work permanently impossible, or a codec change leaving envelopes nobody can decode — while Broker\Nats answered the same call with its dead stream.
This changes what
$publisher->getSize(failed: true)returns. A deployment with old dead letters will see that number rise on upgrade without a single new failure, and anything asserting a threshold against it — Appwrite's/v1/health/queue/failed/{name}endpoint does — can flip to unhealthy on the strength of the upgrade alone. Check your thresholds before rolling this out.
TypeError and ValueError escaping a handler are now treated as permanent, without the handler saying so. The payload and the signature disagree, and they will disagree identically on every delivery — every attempt after the first spends the redelivery budget to reach the conclusion the first one already reached.
On JetStream that is not merely wasteful: a rejected message holds one of the consumer's maxAckPending slots for the whole of its backoff, so enough of them leave the consumer no slot to deliver into and the queue stops for the healthy work behind them. Measured on a staging fleet: 701 messages whose payload carried an object where the handler constructs from an array each burned maxDeliver=5 over ~22 minutes of backoff while holding one of 60 slots, and that worker delivered nothing else for hours.
Deliberately narrow. Every other \Error keeps its budget — OutOfMemoryError and the stack overflow say the host was short at that moment, not that the work is impossible, and that is what redelivery is for.
Broker\Redis::reject() no longer routes a terminal message to the dead list. It stays on failed, with every other rejection.
The verdict buys an ack slot back, and Redis has no such ceiling to protect — a rejected message is already out of the way there, and nothing re-runs it until an operator sweeps. Routing it to dead only put it beyond retry(), which reads failed and is the only recovery path that exists. Broker\Nats is unchanged: it still dead-letters at once, onto a stream retry() can re-drive.
This makes a codec migration recoverable on both transports: flip the writer, let the payloads that no longer fit their signatures fail fast, fix the handlers, re-drive.
getFailedCount() to any class implementing Publisher\Synchronous.getQueueSize(), publish(), publishMany(), retry() or the Consumer interface needs to change.test(queue): settle the claim on the sweep's first read instead of matching the payload key
Queue 5 separates processing concurrency from prefetch:
Queue 5 separates processing concurrency from prefetch:
$server->job('stats-usage', coroutines: 1, prefetch: 100);Prefetch limits all unacknowledged messages, including waiting work and pending confirmations. It defaults to the coroutine count and cannot be lower. Redis groups reservations, completions, and heartbeats while retaining existing opaque list payloads. NATS batches independent acknowledgement requests with a server confirmation for every message. Shutdown returns prefetched work whose handler never started.
Synchronous publishers now consistently use publish() and publishMany(). enqueue() remains the background handoff API. Rename enqueueMany() callers and custom synchronous publisher implementations to publishMany().
This release supersedes the withdrawn 5.0.0 release.
maxCoroutines / batch named arguments and adapter queue-spec keys to coroutines / prefetch. Positional argument order is unchanged. Replace Server::batch() with Server::prefetch().execute(). Redis Cluster namespaces require a shared hash tag; an existing namespace change requires draining or migrating its keys.jobTtl applies to payloads retained after rejection or dead-lettering.utopia-php/nats ^1.4. Existing third-party single-message renewal remains supported.See the design and rollout guide for compatibility details.
Queue 4.0.0 unifies single and batch consumption and adds scheduled recovery of stranded Redis claims.
Queue 4.0.0 unifies single and batch consumption and adds scheduled recovery of stranded Redis claims.
Consumer::receive() now always returns a list:
$messages = $consumer->receive($queue, $timeout, n: 8);
$message = $consumer->receive($queue, $timeout)[0] ?? null;receiveBatch($queue, $timeout, $n) with receive($queue, $timeout, $n).Consumer\Batched is removed. Custom consumers must implement receive(Queue $queue, int $timeout, int $n = 1): array.Connection implementations must implement setNotExists(string $key, string $value, int $ttl = 0): bool with atomic set-if-absent semantics and expiry when requested.null; stored empty strings remain unchanged.The default Redis reapAfter remains 90,000 seconds (25 hours since publication). An expired heartbeat alone does not bypass this age gate. Keep the default conservative until every relevant worker heartbeats.
Recovery needs a running maintenance path. A pooled broker is swept only while idle: continuous blocking receives can prevent recovery in Appwrite's shared size-one pool. Cloud's dedicated receive connection and separate locked commands connection passed a focused Dragonfly recovery check. KubernetesJob workers do not run the Swoole heartbeat/maintenance scheduler; retain their existing recovery mechanism.
Appwrite and Cloud require coordinated consumer/test migrations and dependency updates before adoption. In particular, the currently pinned utopia-php/platform 1.0.0-rc21 does not allow queue 4.x; its dependency constraint also needs updating. This release does not update or deploy either application. See the consumer compatibility audit.
Queue and linked-platform CI passed on the merged commit. The release source also passed local queue checks (Pint, PHPStan, Rector), 183 unit tests / 437 assertions before the final test-only adjustment, and the final Redis expiry regressions on standalone Redis and Redis Cluster, including injected scheduling delays.
Merge pull request #293 from utopia-php/feat/queue-batched-consume
Merge pull request #262 from utopia-php/fix/queue-swoole-stop-before-receive
test(queue): assert auth rejection by its side effect, not Redis wording
Merge pull request #258 from utopia-php/fix/queue-redis-recovers-failed-client
Merge pull request #250 from utopia-php/chore/validators-1-consumers
docs(queue): say that Consumer\Exclusive is a contract callers can read
Merge pull request #230 from utopia-php/fix/queue-refuse-concurrent-exclusive-consumer
fix(queue): restart exited Swoole workers
fix(queue): retain validators 0.6 compatibility
feat(queue): batch background publishes
Merge pull request #200 from utopia-php/fix/queue-pools-maintain-constraint
Merge pull request #194 from utopia-php/chore/no-package-lock-files
chore: move servers, console, http and queue onto validators ^1.0
Merge pull request #170 from utopia-php/fix/queue-message-context-cleanup
Changes : utopia-php/monorepo@ queue/1.5.0...queue/1.5.1
feat(queue,nats): consumer backoff, stream storage choice, dead-letter TTL
feat(queue): publish many messages in one round trip
test(queue): pump dead-lettering via receive(), not getQueueSize()
Merge remote-tracking branch 'origin/main' into cursor/multi-job-queue-platform-f0c8
test(queue): pump dead-lettering via receive(), not getQueueSize()
fix(queue): don't store depth gauges in unused properties
Merge pull request #135 from utopia-php/feat/nats-idiomatic-naming
Merge pull request #138 from utopia-php/feat/queue-duration-buckets
Changes : utopia-php/monorepo@ queue/1.2.0...queue/1.2.1
Merge pull request #126 from utopia-php/queue-nats-broker
chore(queue): allow utopia-php/lock 0.3
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →