[13.x] Add first-class Redis Cluster support for Queue and ConcurrencyLimiter#59533
Merged
Merged
Conversation
This was referenced Apr 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When using AWS ElastiCache Serverless (Valkey) or any Redis Cluster deployment, Laravel's Redis
QueueandConcurrencyLimiterfail withCROSSSLOTerrors because their Lua scripts operate on keys that hash to different cluster slots. Even the Laravel Redis tests explicitly setREDIS_QUEUE: {default}to work-around this issue.For example, the queue
pop()Lua script atomically reads fromqueues:default, writes toqueues:default:reserved, and pops fromqueues:default:notify— three keys that land on three different slots.This PR makes these subsystems cluster-safe by automatically wrapping queue names in Redis hash tags when the connection is a cluster, ensuring all related keys hash to the same slot:
queues:{default} → slot for "default"
queues:{default}:delayed → slot for "default"
queues:{default}:reserved → slot for "default"
queues:{default}:notify → slot for "default"
Different queues (
{emails},{notifications},{exports}) still distribute across the cluster naturally. This is thesame pattern used by Sidekiq, BullMQ, and other cluster-aware queue systems.
Changes
Redis Queue (
RedisQueue)getRedisKey()method that wraps queue names in{...}hash tags when on a cluster connectiongetQueue()unchanged — payload hooks and events still see the original format, which avoids breaking Horizon or other packages that consume the queue namegetQueue()withgetRedisKey()in all Redis operation methods (pushRaw,laterRaw,size,pop,clear, etc.)isClusterConnection()with cached result for the lifetime of the queue instanceConcurrencyLimiter
{limiter}1,{limiter}2instead oflimiter1,limiter2) so themgetLua script doesn't cross slotsrelease()uses the correct keyConnection infrastructure
Connection::isCluster()method (returnsfalseby default,trueinPhpRedisClusterConnectionandPredisClusterConnection)Connection::hasHashTag()static helper for detecting existing{...}hash tags in key namesPhpRedis cluster connector
['username', 'password']array format) for cluster connections viaformatClusterPassword(), gated to phpredis ≥ 5.3.2max_retries,backoff_algorithm,backoff_base,backoff_capoptions to cluster instances (parity with standalone connections) — important for ElastiCache Serverless which returns transient errors during scalingBackward Compatibility
getRedisKey()returns the same keys asgetQueue()when not on a cluster.REDIS_QUEUE={default}: The existing workaround continues to work — hash tag detection prevents double-wrapping.getQueue()is unchanged, so Horizon's payload hooks and event listeners see the same queue name format.Horizon's
readyNow()method would need a one-line update in a follow-up Horizon PR to call the newgetRedisKey().