diff --git a/Adapter/AbstractAdapter.php b/Adapter/AbstractAdapter.php
index c03868da..2b4bc8b2 100644
--- a/Adapter/AbstractAdapter.php
+++ b/Adapter/AbstractAdapter.php
@@ -19,11 +19,12 @@
 use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
 use Symfony\Component\Cache\Traits\ContractsTrait;
 use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 
 /**
  * @author Nicolas Grekas <p@tchwork.com>
  */
-abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
+abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
 {
     use AbstractAdapterTrait;
     use ContractsTrait;
@@ -37,7 +38,19 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
 
     protected function __construct(string $namespace = '', int $defaultLifetime = 0)
     {
-        $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
+        if ('' !== $namespace) {
+            if (str_contains($namespace, static::NS_SEPARATOR)) {
+                if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
+                    throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
+                }
+                CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
+            } else {
+                CacheItem::validateKey($namespace);
+            }
+            $this->namespace = $namespace.static::NS_SEPARATOR;
+        }
+        $this->rootNamespace = $this->namespace;
+
         $this->defaultLifetime = $defaultLifetime;
         if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
             throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
@@ -111,14 +124,14 @@ public static function createSystemCache(string $namespace, int $defaultLifetime
 
     public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
     {
-        if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
+        if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:')) {
             return RedisAdapter::createConnection($dsn, $options);
         }
         if (str_starts_with($dsn, 'memcached:')) {
             return MemcachedAdapter::createConnection($dsn, $options);
         }
         if (str_starts_with($dsn, 'couchbase:')) {
-            if (class_exists('CouchbaseBucket') && CouchbaseBucketAdapter::isSupported()) {
+            if (class_exists(\CouchbaseBucket::class) && CouchbaseBucketAdapter::isSupported()) {
                 return CouchbaseBucketAdapter::createConnection($dsn, $options);
             }
 
@@ -128,7 +141,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
             return PdoAdapter::createConnection($dsn, $options);
         }
 
-        throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
+        throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "valkey[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
     }
 
     public function commit(): bool
@@ -159,7 +172,7 @@ public function commit(): bool
                     $v = $values[$id];
                     $type = get_debug_type($v);
                     $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
-                    CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
+                    CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
                 }
             } else {
                 foreach ($values as $id => $v) {
@@ -182,7 +195,7 @@ public function commit(): bool
                 $ok = false;
                 $type = get_debug_type($v);
                 $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
-                CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
+                CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
             }
         }
 
diff --git a/Adapter/AbstractTagAwareAdapter.php b/Adapter/AbstractTagAwareAdapter.php
index 822c30f0..23db2b6e 100644
--- a/Adapter/AbstractTagAwareAdapter.php
+++ b/Adapter/AbstractTagAwareAdapter.php
@@ -17,6 +17,7 @@
 use Symfony\Component\Cache\ResettableInterface;
 use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
 use Symfony\Component\Cache\Traits\ContractsTrait;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 use Symfony\Contracts\Cache\TagAwareCacheInterface;
 
 /**
@@ -30,16 +31,33 @@
  *
  * @internal
  */
-abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
+abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, AdapterInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
 {
     use AbstractAdapterTrait;
     use ContractsTrait;
 
+    /**
+     * @internal
+     */
+    protected const NS_SEPARATOR = ':';
+
     private const TAGS_PREFIX = "\1tags\1";
 
     protected function __construct(string $namespace = '', int $defaultLifetime = 0)
     {
-        $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
+        if ('' !== $namespace) {
+            if (str_contains($namespace, static::NS_SEPARATOR)) {
+                if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
+                    throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
+                }
+                CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
+            } else {
+                CacheItem::validateKey($namespace);
+            }
+            $this->namespace = $namespace.static::NS_SEPARATOR;
+        }
+        $this->rootNamespace = $this->namespace;
+
         $this->defaultLifetime = $defaultLifetime;
         if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
             throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
@@ -70,7 +88,7 @@ static function ($key, $value, $isHit) {
             CacheItem::class
         );
         self::$mergeByLifetime ??= \Closure::bind(
-            static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
+            static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime, $rootNamespace) {
                 $byLifetime = [];
                 $now = microtime(true);
                 $expiredIds = [];
@@ -102,10 +120,10 @@ static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime)
                     $value['tag-operations'] = ['add' => [], 'remove' => []];
                     $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
                     foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
-                        $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
+                        $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag, $rootNamespace);
                     }
                     foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
-                        $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
+                        $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag, $rootNamespace);
                     }
                     $value['tags'] = array_keys($value['tags']);
 
@@ -168,7 +186,7 @@ protected function doDeleteYieldTags(array $ids): iterable
     public function commit(): bool
     {
         $ok = true;
-        $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
+        $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime, $this->rootNamespace);
         $retry = $this->deferred = [];
 
         if ($expiredIds) {
@@ -195,7 +213,7 @@ public function commit(): bool
                     $v = $values[$id];
                     $type = get_debug_type($v);
                     $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
-                    CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
+                    CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
                 }
             } else {
                 foreach ($values as $id => $v) {
@@ -219,7 +237,7 @@ public function commit(): bool
                 $ok = false;
                 $type = get_debug_type($v);
                 $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
-                CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
+                CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
             }
         }
 
@@ -244,7 +262,7 @@ public function deleteItems(array $keys): bool
         try {
             foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
                 foreach ($tags as $tag) {
-                    $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
+                    $tagData[$this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace)][] = $id;
                 }
             }
         } catch (\Exception) {
@@ -283,7 +301,7 @@ public function invalidateTags(array $tags): bool
 
         $tagIds = [];
         foreach (array_unique($tags) as $tag) {
-            $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
+            $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace);
         }
 
         try {
diff --git a/Adapter/ArrayAdapter.php b/Adapter/ArrayAdapter.php
index 8941ae1c..c9ee94ee 100644
--- a/Adapter/ArrayAdapter.php
+++ b/Adapter/ArrayAdapter.php
@@ -19,6 +19,7 @@
 use Symfony\Component\Cache\Exception\InvalidArgumentException;
 use Symfony\Component\Cache\ResettableInterface;
 use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 
 /**
  * An in-memory cache storage.
@@ -27,13 +28,14 @@
  *
  * @author Nicolas Grekas <p@tchwork.com>
  */
-class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
+class ArrayAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
 {
     use LoggerAwareTrait;
 
     private array $values = [];
     private array $tags = [];
     private array $expiries = [];
+    private array $subPools = [];
 
     private static \Closure $createCacheItem;
 
@@ -226,16 +228,38 @@ public function clear(string $prefix = ''): bool
                 }
             }
 
-            if ($this->values) {
-                return true;
-            }
+            return true;
+        }
+
+        foreach ($this->subPools as $pool) {
+            $pool->clear();
         }
 
-        $this->values = $this->tags = $this->expiries = [];
+        $this->subPools = $this->values = $this->tags = $this->expiries = [];
 
         return true;
     }
 
+    public function withSubNamespace(string $namespace): static
+    {
+        CacheItem::validateKey($namespace);
+
+        $subPools = $this->subPools;
+
+        if (isset($subPools[$namespace])) {
+            return $subPools[$namespace];
+        }
+
+        $this->subPools = [];
+        $clone = clone $this;
+        $clone->clear();
+
+        $subPools[$namespace] = $clone;
+        $this->subPools = $subPools;
+
+        return $clone;
+    }
+
     /**
      * Returns all cached values, with cache miss as null.
      */
@@ -263,6 +287,13 @@ public function reset(): void
         $this->clear();
     }
 
+    public function __clone()
+    {
+        foreach ($this->subPools as $i => $pool) {
+            $this->subPools[$i] = clone $pool;
+        }
+    }
+
     private function generateItems(array $keys, float $now, \Closure $f): \Generator
     {
         foreach ($keys as $i => $key) {
diff --git a/Adapter/ChainAdapter.php b/Adapter/ChainAdapter.php
index 09fcfdcc..c27faeb1 100644
--- a/Adapter/ChainAdapter.php
+++ b/Adapter/ChainAdapter.php
@@ -14,11 +14,13 @@
 use Psr\Cache\CacheItemInterface;
 use Psr\Cache\CacheItemPoolInterface;
 use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\BadMethodCallException;
 use Symfony\Component\Cache\Exception\InvalidArgumentException;
 use Symfony\Component\Cache\PruneableInterface;
 use Symfony\Component\Cache\ResettableInterface;
 use Symfony\Component\Cache\Traits\ContractsTrait;
 use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 use Symfony\Contracts\Service\ResetInterface;
 
 /**
@@ -29,7 +31,7 @@
  *
  * @author Kévin Dunglas <dunglas@gmail.com>
  */
-class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
+class ChainAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
 {
     use ContractsTrait;
 
@@ -280,6 +282,23 @@ public function prune(): bool
         return $pruned;
     }
 
+    public function withSubNamespace(string $namespace): static
+    {
+        $clone = clone $this;
+        $adapters = [];
+
+        foreach ($this->adapters as $adapter) {
+            if (!$adapter instanceof NamespacedPoolInterface) {
+                throw new BadMethodCallException('All adapters must implement NamespacedPoolInterface to support namespaces.');
+            }
+
+            $adapters[] = $adapter->withSubNamespace($namespace);
+        }
+        $clone->adapters = $adapters;
+
+        return $clone;
+    }
+
     public function reset(): void
     {
         foreach ($this->adapters as $adapter) {
diff --git a/Adapter/DoctrineDbalAdapter.php b/Adapter/DoctrineDbalAdapter.php
index d67464a4..c3a4909e 100644
--- a/Adapter/DoctrineDbalAdapter.php
+++ b/Adapter/DoctrineDbalAdapter.php
@@ -338,17 +338,17 @@ protected function doSave(array $values, int $lifetime): array|bool
     /**
      * @internal
      */
-    protected function getId(mixed $key): string
+    protected function getId(mixed $key, ?string $namespace = null): string
     {
         if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
-            return parent::getId($key);
+            return parent::getId($key, $namespace);
         }
 
         if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
             $key = rawurlencode($key);
         }
 
-        return parent::getId($key);
+        return parent::getId($key, $namespace);
     }
 
     private function getPlatformName(): string
diff --git a/Adapter/NullAdapter.php b/Adapter/NullAdapter.php
index d5d2ef6b..35553ea1 100644
--- a/Adapter/NullAdapter.php
+++ b/Adapter/NullAdapter.php
@@ -14,11 +14,12 @@
 use Psr\Cache\CacheItemInterface;
 use Symfony\Component\Cache\CacheItem;
 use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 
 /**
  * @author Titouan Galopin <galopintitouan@gmail.com>
  */
-class NullAdapter implements AdapterInterface, CacheInterface
+class NullAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface
 {
     private static \Closure $createCacheItem;
 
@@ -94,6 +95,11 @@ public function delete(string $key): bool
         return $this->deleteItem($key);
     }
 
+    public function withSubNamespace(string $namespace): static
+    {
+        return clone $this;
+    }
+
     private function generateItems(array $keys): \Generator
     {
         $f = self::$createCacheItem;
diff --git a/Adapter/PdoAdapter.php b/Adapter/PdoAdapter.php
index 525e2c6d..7d6cb2df 100644
--- a/Adapter/PdoAdapter.php
+++ b/Adapter/PdoAdapter.php
@@ -348,17 +348,17 @@ protected function doSave(array $values, int $lifetime): array|bool
     /**
      * @internal
      */
-    protected function getId(mixed $key): string
+    protected function getId(mixed $key, ?string $namespace = null): string
     {
         if ('pgsql' !== $this->getDriver()) {
-            return parent::getId($key);
+            return parent::getId($key, $namespace);
         }
 
         if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
             $key = rawurlencode($key);
         }
 
-        return parent::getId($key);
+        return parent::getId($key, $namespace);
     }
 
     private function getConnection(): \PDO
diff --git a/Adapter/PhpFilesAdapter.php b/Adapter/PhpFilesAdapter.php
index df0d0e71..e64ec4c3 100644
--- a/Adapter/PhpFilesAdapter.php
+++ b/Adapter/PhpFilesAdapter.php
@@ -103,65 +103,65 @@ protected function doFetch(array $ids): iterable
         }
         $values = [];
 
-        begin:
-        $getExpiry = false;
-
-        foreach ($ids as $id) {
-            if (null === $value = $this->values[$id] ?? null) {
-                $missingIds[] = $id;
-            } elseif ('N;' === $value) {
-                $values[$id] = null;
-            } elseif (!\is_object($value)) {
-                $values[$id] = $value;
-            } elseif (!$value instanceof LazyValue) {
-                $values[$id] = $value();
-            } elseif (false === $values[$id] = include $value->file) {
-                unset($values[$id], $this->values[$id]);
-                $missingIds[] = $id;
+        while (true) {
+            $getExpiry = false;
+
+            foreach ($ids as $id) {
+                if (null === $value = $this->values[$id] ?? null) {
+                    $missingIds[] = $id;
+                } elseif ('N;' === $value) {
+                    $values[$id] = null;
+                } elseif (!\is_object($value)) {
+                    $values[$id] = $value;
+                } elseif (!$value instanceof LazyValue) {
+                    $values[$id] = $value();
+                } elseif (false === $values[$id] = include $value->file) {
+                    unset($values[$id], $this->values[$id]);
+                    $missingIds[] = $id;
+                }
+                if (!$this->appendOnly) {
+                    unset($this->values[$id]);
+                }
             }
-            if (!$this->appendOnly) {
-                unset($this->values[$id]);
+
+            if (!$missingIds) {
+                return $values;
             }
-        }
 
-        if (!$missingIds) {
-            return $values;
-        }
+            set_error_handler($this->includeHandler);
+            try {
+                $getExpiry = true;
 
-        set_error_handler($this->includeHandler);
-        try {
-            $getExpiry = true;
+                foreach ($missingIds as $k => $id) {
+                    try {
+                        $file = $this->files[$id] ??= $this->getFile($id);
 
-            foreach ($missingIds as $k => $id) {
-                try {
-                    $file = $this->files[$id] ??= $this->getFile($id);
+                        if (isset(self::$valuesCache[$file])) {
+                            [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
+                        } elseif (\is_array($expiresAt = include $file)) {
+                            if ($this->appendOnly) {
+                                self::$valuesCache[$file] = $expiresAt;
+                            }
 
-                    if (isset(self::$valuesCache[$file])) {
-                        [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
-                    } elseif (\is_array($expiresAt = include $file)) {
-                        if ($this->appendOnly) {
-                            self::$valuesCache[$file] = $expiresAt;
+                            [$expiresAt, $this->values[$id]] = $expiresAt;
+                        } elseif ($now < $expiresAt) {
+                            $this->values[$id] = new LazyValue($file);
                         }
 
-                        [$expiresAt, $this->values[$id]] = $expiresAt;
-                    } elseif ($now < $expiresAt) {
-                        $this->values[$id] = new LazyValue($file);
-                    }
-
-                    if ($now >= $expiresAt) {
-                        unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
+                        if ($now >= $expiresAt) {
+                            unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
+                        }
+                    } catch (\ErrorException $e) {
+                        unset($missingIds[$k]);
                     }
-                } catch (\ErrorException $e) {
-                    unset($missingIds[$k]);
                 }
+            } finally {
+                restore_error_handler();
             }
-        } finally {
-            restore_error_handler();
-        }
 
-        $ids = $missingIds;
-        $missingIds = [];
-        goto begin;
+            $ids = $missingIds;
+            $missingIds = [];
+        }
     }
 
     protected function doHave(string $id): bool
diff --git a/Adapter/ProxyAdapter.php b/Adapter/ProxyAdapter.php
index 56212260..d692dbf3 100644
--- a/Adapter/ProxyAdapter.php
+++ b/Adapter/ProxyAdapter.php
@@ -19,11 +19,12 @@
 use Symfony\Component\Cache\Traits\ContractsTrait;
 use Symfony\Component\Cache\Traits\ProxyTrait;
 use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 
 /**
  * @author Nicolas Grekas <p@tchwork.com>
  */
-class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
+class ProxyAdapter implements AdapterInterface, NamespacedPoolInterface, CacheInterface, PruneableInterface, ResettableInterface
 {
     use ContractsTrait;
     use ProxyTrait;
@@ -38,12 +39,17 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
 
     public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
     {
-        $this->pool = $pool;
-        $this->poolHash = spl_object_hash($pool);
         if ('' !== $namespace) {
-            \assert('' !== CacheItem::validateKey($namespace));
-            $this->namespace = $namespace;
+            if ($pool instanceof NamespacedPoolInterface) {
+                $pool = $pool->withSubNamespace($namespace);
+                $this->namespace = $namespace = '';
+            } else {
+                \assert('' !== CacheItem::validateKey($namespace));
+                $this->namespace = $namespace;
+            }
         }
+        $this->pool = $pool;
+        $this->poolHash = spl_object_hash($pool);
         $this->namespaceLen = \strlen($namespace);
         $this->defaultLifetime = $defaultLifetime;
         self::$createCacheItem ??= \Closure::bind(
@@ -158,6 +164,20 @@ public function commit(): bool
         return $this->pool->commit();
     }
 
+    public function withSubNamespace(string $namespace): static
+    {
+        $clone = clone $this;
+
+        if ($clone->pool instanceof NamespacedPoolInterface) {
+            $clone->pool = $clone->pool->withSubNamespace($namespace);
+        } else {
+            $clone->namespace .= CacheItem::validateKey($namespace);
+            $clone->namespaceLen = \strlen($clone->namespace);
+        }
+
+        return $clone;
+    }
+
     private function doSave(CacheItemInterface $item, string $method): bool
     {
         if (!$item instanceof CacheItem) {
diff --git a/Adapter/RedisAdapter.php b/Adapter/RedisAdapter.php
index e33f2f65..f31f0d7d 100644
--- a/Adapter/RedisAdapter.php
+++ b/Adapter/RedisAdapter.php
@@ -18,7 +18,7 @@ class RedisAdapter extends AbstractAdapter
 {
     use RedisTrait;
 
-    public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
+    public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay|\Relay\Cluster $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
     {
         $this->init($redis, $namespace, $defaultLifetime, $marshaller);
     }
diff --git a/Adapter/RedisTagAwareAdapter.php b/Adapter/RedisTagAwareAdapter.php
index 7b282375..779c4d91 100644
--- a/Adapter/RedisTagAwareAdapter.php
+++ b/Adapter/RedisTagAwareAdapter.php
@@ -60,7 +60,7 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
     private string $redisEvictionPolicy;
 
     public function __construct(
-        \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
+        \Redis|Relay|\Relay\Cluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
         private string $namespace = '',
         int $defaultLifetime = 0,
         ?MarshallerInterface $marshaller = null,
@@ -69,7 +69,7 @@ public function __construct(
             throw new InvalidArgumentException(\sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
         }
 
-        $isRelay = $redis instanceof Relay;
+        $isRelay = $redis instanceof Relay || $redis instanceof \Relay\Cluster;
         if ($isRelay || \defined('Redis::OPT_COMPRESSION') && \in_array($redis::class, [\Redis::class, \RedisArray::class, \RedisCluster::class], true)) {
             $compression = $redis->getOption($isRelay ? Relay::OPT_COMPRESSION : \Redis::OPT_COMPRESSION);
 
@@ -159,7 +159,7 @@ protected function doDeleteYieldTags(array $ids): iterable
 
         foreach ($results as $id => $result) {
             if ($result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) {
-                CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);
+                CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $result]);
 
                 continue;
             }
@@ -225,7 +225,7 @@ protected function doInvalidate(array $tagIds): bool
         $results = $this->pipeline(function () use ($tagIds, $lua) {
             if ($this->redis instanceof \Predis\ClientInterface) {
                 $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
-            } elseif (\is_array($prefix = $this->redis->getOption($this->redis instanceof Relay ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
+            } elseif (\is_array($prefix = $this->redis->getOption(($this->redis instanceof Relay || $this->redis instanceof \Relay\Cluster) ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
                 $prefix = current($prefix);
             }
 
diff --git a/Adapter/TagAwareAdapter.php b/Adapter/TagAwareAdapter.php
index 53c98904..70927cf4 100644
--- a/Adapter/TagAwareAdapter.php
+++ b/Adapter/TagAwareAdapter.php
@@ -16,9 +16,11 @@
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\BadMethodCallException;
 use Symfony\Component\Cache\PruneableInterface;
 use Symfony\Component\Cache\ResettableInterface;
 use Symfony\Component\Cache\Traits\ContractsTrait;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 use Symfony\Contracts\Cache\TagAwareCacheInterface;
 
 /**
@@ -33,7 +35,7 @@
  * @author Nicolas Grekas <p@tchwork.com>
  * @author Sergey Belyshkin <sbelyshkin@gmail.com>
  */
-class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
+class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
 {
     use ContractsTrait;
     use LoggerAwareTrait;
@@ -277,6 +279,23 @@ public function commit(): bool
         return $ok;
     }
 
+    /**
+     * @throws BadMethodCallException When the item pool is not a NamespacedPoolInterface
+     */
+    public function withSubNamespace(string $namespace): static
+    {
+        if (!$this->pool instanceof NamespacedPoolInterface) {
+            throw new BadMethodCallException(\sprintf('Cannot call "%s::withSubNamespace()": this class doesn\'t implement "%s".', get_debug_type($this->pool), NamespacedPoolInterface::class));
+        }
+
+        $knownTagVersions = &$this->knownTagVersions; // ensures clones share the same array
+        $clone = clone $this;
+        $clone->deferred = [];
+        $clone->pool = $this->pool->withSubNamespace($namespace);
+
+        return $clone;
+    }
+
     public function prune(): bool
     {
         return $this->pool instanceof PruneableInterface && $this->pool->prune();
diff --git a/Adapter/TraceableAdapter.php b/Adapter/TraceableAdapter.php
index 8fe6cf37..43628e4c 100644
--- a/Adapter/TraceableAdapter.php
+++ b/Adapter/TraceableAdapter.php
@@ -13,9 +13,11 @@
 
 use Psr\Cache\CacheItemInterface;
 use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\BadMethodCallException;
 use Symfony\Component\Cache\PruneableInterface;
 use Symfony\Component\Cache\ResettableInterface;
 use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 use Symfony\Contracts\Service\ResetInterface;
 
 /**
@@ -25,8 +27,9 @@
  * @author Tobias Nyholm <tobias.nyholm@gmail.com>
  * @author Nicolas Grekas <p@tchwork.com>
  */
-class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
+class TraceableAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
 {
+    private string $namespace = '';
     private array $calls = [];
 
     public function __construct(
@@ -34,10 +37,13 @@ public function __construct(
     ) {
     }
 
+    /**
+     * @throws BadMethodCallException When the item pool is not a CacheInterface
+     */
     public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
     {
         if (!$this->pool instanceof CacheInterface) {
-            throw new \BadMethodCallException(\sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
+            throw new BadMethodCallException(\sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
         }
 
         $isHit = true;
@@ -225,11 +231,29 @@ public function getPool(): AdapterInterface
         return $this->pool;
     }
 
+    /**
+     * @throws BadMethodCallException When the item pool is not a NamespacedPoolInterface
+     */
+    public function withSubNamespace(string $namespace): static
+    {
+        if (!$this->pool instanceof NamespacedPoolInterface) {
+            throw new BadMethodCallException(\sprintf('Cannot call "%s::withSubNamespace()": this class doesn\'t implement "%s".', get_debug_type($this->pool), NamespacedPoolInterface::class));
+        }
+
+        $calls = &$this->calls; // ensures clones share the same array
+        $clone = clone $this;
+        $clone->namespace .= CacheItem::validateKey($namespace).':';
+        $clone->pool = $this->pool->withSubNamespace($namespace);
+
+        return $clone;
+    }
+
     protected function start(string $name): TraceableAdapterEvent
     {
         $this->calls[] = $event = new TraceableAdapterEvent();
         $event->name = $name;
         $event->start = microtime(true);
+        $event->namespace = $this->namespace;
 
         return $event;
     }
@@ -246,4 +270,5 @@ class TraceableAdapterEvent
     public array|bool $result;
     public int $hits = 0;
     public int $misses = 0;
+    public string $namespace;
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 038915c4..d7b18246 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
 CHANGELOG
 =========
 
+7.3
+---
+
+ * Add support for `\Relay\Cluster` in `RedisAdapter`
+ * Add support for `valkey:` / `valkeys:` schemes
+ * Add support for namespace-based invalidation
+ * Rename options "redis_cluster" and "redis_sentinel" to "cluster" and "sentinel" respectively
+
 7.2
 ---
 
diff --git a/Exception/BadMethodCallException.php b/Exception/BadMethodCallException.php
new file mode 100644
index 00000000..d81f9d26
--- /dev/null
+++ b/Exception/BadMethodCallException.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Exception;
+
+use Psr\Cache\CacheException as Psr6CacheInterface;
+use Psr\SimpleCache\CacheException as SimpleCacheInterface;
+
+if (interface_exists(SimpleCacheInterface::class)) {
+    class BadMethodCallException extends \BadMethodCallException implements Psr6CacheInterface, SimpleCacheInterface
+    {
+    }
+} else {
+    class BadMethodCallException extends \BadMethodCallException implements Psr6CacheInterface
+    {
+    }
+}
diff --git a/Tests/Adapter/AbstractRedisAdapterTestCase.php b/Tests/Adapter/AbstractRedisAdapterTestCase.php
index c83365cc..c139cc97 100644
--- a/Tests/Adapter/AbstractRedisAdapterTestCase.php
+++ b/Tests/Adapter/AbstractRedisAdapterTestCase.php
@@ -12,6 +12,7 @@
 namespace Symfony\Component\Cache\Tests\Adapter;
 
 use Psr\Cache\CacheItemPoolInterface;
+use Relay\Cluster as RelayCluster;
 use Relay\Relay;
 use Symfony\Component\Cache\Adapter\RedisAdapter;
 
@@ -23,7 +24,7 @@ abstract class AbstractRedisAdapterTestCase extends AdapterTestCase
         'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
     ];
 
-    protected static \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
+    protected static \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
 
     public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
     {
diff --git a/Tests/Adapter/AdapterTestCase.php b/Tests/Adapter/AdapterTestCase.php
index 031191ca..896ca94a 100644
--- a/Tests/Adapter/AdapterTestCase.php
+++ b/Tests/Adapter/AdapterTestCase.php
@@ -18,6 +18,7 @@
 use Symfony\Component\Cache\CacheItem;
 use Symfony\Component\Cache\PruneableInterface;
 use Symfony\Contracts\Cache\CallbackInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
 
 abstract class AdapterTestCase extends CachePoolTest
 {
@@ -367,6 +368,33 @@ public function testErrorsDontInvalidate()
         $this->assertFalse($cache->save($item));
         $this->assertSame('bar', $cache->getItem('foo')->get());
     }
+
+    public function testNamespaces()
+    {
+        if (isset($this->skippedTests[__FUNCTION__])) {
+            $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+        }
+
+        $cache = $this->createCachePool(0, __FUNCTION__);
+
+        $this->assertInstanceOf(NamespacedPoolInterface::class, $cache);
+
+        $derived = $cache->withSubNamespace('derived');
+
+        $item = $derived->getItem('foo');
+        $derived->save($item->set('Foo'));
+
+        $this->assertFalse($cache->getItem('foo')->isHit());
+
+        $item = $cache->getItem('bar');
+        $cache->save($item->set('Bar'));
+
+        $this->assertFalse($derived->getItem('bar')->isHit());
+        $this->assertTrue($cache->getItem('bar')->isHit());
+
+        $derived = $cache->withSubNamespace('derived');
+        $this->assertTrue($derived->getItem('foo')->isHit());
+    }
 }
 
 class NotUnserializable
diff --git a/Tests/Adapter/DoctrineDbalAdapterTest.php b/Tests/Adapter/DoctrineDbalAdapterTest.php
index 79752f39..db20b0f3 100644
--- a/Tests/Adapter/DoctrineDbalAdapterTest.php
+++ b/Tests/Adapter/DoctrineDbalAdapterTest.php
@@ -117,7 +117,7 @@ public function testConfigureSchemaTableExists()
         $adapter = new DoctrineDbalAdapter($connection);
         $adapter->configureSchema($schema, $connection, fn () => true);
         $table = $schema->getTable('cache_items');
-        $this->assertEmpty($table->getColumns(), 'The table was not overwritten');
+        $this->assertSame([], $table->getColumns(), 'The table was not overwritten');
     }
 
     /**
diff --git a/Tests/Adapter/PhpArrayAdapterTest.php b/Tests/Adapter/PhpArrayAdapterTest.php
index ada3149d..0c856e6f 100644
--- a/Tests/Adapter/PhpArrayAdapterTest.php
+++ b/Tests/Adapter/PhpArrayAdapterTest.php
@@ -58,6 +58,8 @@ class PhpArrayAdapterTest extends AdapterTestCase
 
         'testDefaultLifeTime' => 'PhpArrayAdapter does not allow configuring a default lifetime.',
         'testPrune' => 'PhpArrayAdapter just proxies',
+
+        'testNamespaces' => 'PhpArrayAdapter does not support namespaces.',
     ];
 
     protected static string $file;
diff --git a/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
index d20ffd55..0f92aee4 100644
--- a/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
+++ b/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
@@ -28,6 +28,8 @@ class PhpArrayAdapterWithFallbackTest extends AdapterTestCase
         'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.',
         'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.',
         'testPrune' => 'PhpArrayAdapter just proxies',
+
+        'testNamespaces' => 'PhpArrayAdapter does not support namespaces.',
     ];
 
     protected static string $file;
diff --git a/Tests/Adapter/PredisAdapterTest.php b/Tests/Adapter/PredisAdapterTest.php
index d9afd85a..730bde71 100644
--- a/Tests/Adapter/PredisAdapterTest.php
+++ b/Tests/Adapter/PredisAdapterTest.php
@@ -42,7 +42,7 @@ public function testCreateConnection()
             'scheme' => 'tcp',
             'host' => $redisHost[0],
             'port' => (int) ($redisHost[1] ?? 6379),
-            'persistent' => 0,
+            'persistent' => false,
             'timeout' => 3,
             'read_write_timeout' => 0,
             'tcp_nodelay' => true,
@@ -74,7 +74,7 @@ public function testCreateSslConnection()
             'host' => $redisHost[0],
             'port' => (int) ($redisHost[1] ?? 6379),
             'ssl' => ['verify_peer' => '0'],
-            'persistent' => 0,
+            'persistent' => false,
             'timeout' => 3,
             'read_write_timeout' => 0,
             'tcp_nodelay' => true,
diff --git a/Tests/Adapter/PredisReplicationAdapterTest.php b/Tests/Adapter/PredisReplicationAdapterTest.php
index 28af1b5b..b9877234 100644
--- a/Tests/Adapter/PredisReplicationAdapterTest.php
+++ b/Tests/Adapter/PredisReplicationAdapterTest.php
@@ -27,9 +27,9 @@ public static function setUpBeforeClass(): void
         $hosts = explode(' ', getenv('REDIS_REPLICATION_HOSTS'));
         $lastArrayKey = array_key_last($hosts);
         $hostTable = [];
-        foreach($hosts as $key => $host) {
+        foreach ($hosts as $key => $host) {
             $hostInformation = array_combine(['host', 'port'], explode(':', $host));
-            if($lastArrayKey === $key) {
+            if ($lastArrayKey === $key) {
                 $hostInformation['role'] = 'master';
             }
             $hostTable[] = $hostInformation;
diff --git a/Tests/Adapter/RedisAdapterSentinelTest.php b/Tests/Adapter/RedisAdapterSentinelTest.php
index 6dc13b81..9103eec5 100644
--- a/Tests/Adapter/RedisAdapterSentinelTest.php
+++ b/Tests/Adapter/RedisAdapterSentinelTest.php
@@ -32,15 +32,15 @@ public static function setUpBeforeClass(): void
             self::markTestSkipped('REDIS_SENTINEL_SERVICE env var is not defined.');
         }
 
-        self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']&timeout=0&retry_interval=0&read_timeout=0', ['redis_sentinel' => $service, 'prefix' => 'prefix_']);
+        self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']&timeout=0&retry_interval=0&read_timeout=0', ['sentinel' => $service, 'prefix' => 'prefix_']);
     }
 
     public function testInvalidDSNHasBothClusterAndSentinel()
     {
-        $dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&redis_cluster=1&redis_sentinel=mymaster';
+        $dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&cluster=1&sentinel=mymaster';
 
         $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
+        $this->expectExceptionMessage('Cannot use both "cluster" and "sentinel" at the same time.');
 
         RedisAdapter::createConnection($dsn);
     }
@@ -51,6 +51,6 @@ public function testExceptionMessageWhenFailingToRetrieveMasterInformation()
         $dsn = 'redis:?host['.str_replace(' ', ']&host[', $hosts).']';
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Failed to retrieve master information from sentinel "invalid-masterset-name".');
-        AbstractAdapter::createConnection($dsn, ['redis_sentinel' => 'invalid-masterset-name']);
+        AbstractAdapter::createConnection($dsn, ['sentinel' => 'invalid-masterset-name']);
     }
 }
diff --git a/Tests/Adapter/RedisTagAwareRelayClusterAdapterTest.php b/Tests/Adapter/RedisTagAwareRelayClusterAdapterTest.php
new file mode 100644
index 00000000..4939d2df
--- /dev/null
+++ b/Tests/Adapter/RedisTagAwareRelayClusterAdapterTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use Psr\Cache\CacheItemPoolInterface;
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Traits\RelayClusterProxy;
+
+/**
+ * @requires extension relay
+ *
+ * @group integration
+ */
+class RedisTagAwareRelayClusterAdapterTest extends RelayClusterAdapterTest
+{
+    use TagAwareTestTrait;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+    }
+
+    public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
+    {
+        $this->assertInstanceOf(RelayClusterProxy::class, self::$redis);
+        $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+        return $adapter;
+    }
+}
diff --git a/Tests/Adapter/RelayClusterAdapterTest.php b/Tests/Adapter/RelayClusterAdapterTest.php
new file mode 100644
index 00000000..56363f82
--- /dev/null
+++ b/Tests/Adapter/RelayClusterAdapterTest.php
@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use Psr\Cache\CacheItemPoolInterface;
+use Relay\Cluster as RelayCluster;
+use Relay\Relay;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
+use Symfony\Component\Cache\Adapter\RedisAdapter;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\RelayClusterProxy;
+
+/**
+ * @requires extension relay
+ *
+ * @group integration
+ */
+class RelayClusterAdapterTest extends AbstractRedisAdapterTestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        if (!class_exists(RelayCluster::class)) {
+            self::markTestSkipped('The Relay\Cluster class is required.');
+        }
+        if (!$hosts = getenv('REDIS_CLUSTER_HOSTS')) {
+            self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.');
+        }
+
+        self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['lazy' => true, 'redis_cluster' => true, 'class' => RelayCluster::class]);
+        self::$redis->setOption(Relay::OPT_PREFIX, 'prefix_');
+    }
+
+    public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
+    {
+        $this->assertInstanceOf(RelayClusterProxy::class, self::$redis);
+        $adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+        return $adapter;
+    }
+
+    /**
+     * @dataProvider provideFailedCreateConnection
+     */
+    public function testFailedCreateConnection(string $dsn)
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('Relay cluster connection failed:');
+        RedisAdapter::createConnection($dsn);
+    }
+
+    public static function provideFailedCreateConnection(): array
+    {
+        return [
+            ['redis://localhost:1234?redis_cluster=1&class=Relay\Cluster'],
+            ['redis://foo@localhost?redis_cluster=1&class=Relay\Cluster'],
+            ['redis://localhost/123?redis_cluster=1&class=Relay\Cluster'],
+        ];
+    }
+}
diff --git a/Tests/Adapter/TagAwareTestTrait.php b/Tests/Adapter/TagAwareTestTrait.php
index 8ec1297e..9894ba00 100644
--- a/Tests/Adapter/TagAwareTestTrait.php
+++ b/Tests/Adapter/TagAwareTestTrait.php
@@ -183,4 +183,27 @@ public function testRefreshAfterExpires()
         $cacheItem = $pool->getItem('test');
         $this->assertTrue($cacheItem->isHit());
     }
+
+    public function testNamespacesAndTags()
+    {
+        $pool = $this->createCachePool();
+        $pool->clear();
+
+        $item = $pool->getItem('foo');
+        $item->tag('baz');
+        $pool->save($item);
+
+        $derived = $pool->withSubNamespace('derived');
+        $item = $derived->getItem('bar');
+        $item->tag('baz');
+        $derived->save($item);
+
+        $this->assertTrue($pool->getItem('foo')->isHit());
+        $this->assertTrue($derived->getItem('bar')->isHit());
+
+        $pool->invalidateTags(['baz']);
+
+        $this->assertFalse($pool->getItem('foo')->isHit());
+        $this->assertFalse($derived->getItem('bar')->isHit());
+    }
 }
diff --git a/Tests/Psr16CacheProxyTest.php b/Tests/Psr16CacheProxyTest.php
index c3d2d8d5..fa771cf9 100644
--- a/Tests/Psr16CacheProxyTest.php
+++ b/Tests/Psr16CacheProxyTest.php
@@ -45,12 +45,12 @@ public function createSimpleCache(int $defaultLifetime = 0): CacheInterface
     public function testProxy()
     {
         $pool = new ArrayAdapter();
-        $cache = new Psr16Cache(new ProxyAdapter($pool, 'my-namespace.'));
+        $cache = new Psr16Cache(new ProxyAdapter($pool, 'my-namespace'));
 
         $this->assertNull($cache->get('some-key'));
         $this->assertTrue($cache->set('some-other-key', 'value'));
 
-        $item = $pool->getItem('my-namespace.some-other-key', 'value');
+        $item = $pool->withSubNamespace('my-namespace')->getItem('some-other-key', 'value');
         $this->assertTrue($item->isHit());
         $this->assertSame('value', $item->get());
     }
diff --git a/Tests/Traits/RedisProxiesTest.php b/Tests/Traits/RedisProxiesTest.php
index 1e17b474..50f784da 100644
--- a/Tests/Traits/RedisProxiesTest.php
+++ b/Tests/Traits/RedisProxiesTest.php
@@ -12,10 +12,11 @@
 namespace Symfony\Component\Cache\Tests\Traits;
 
 use PHPUnit\Framework\TestCase;
+use Relay\Cluster as RelayCluster;
 use Relay\Relay;
 use Symfony\Component\Cache\Traits\RedisProxyTrait;
+use Symfony\Component\Cache\Traits\RelayClusterProxy;
 use Symfony\Component\Cache\Traits\RelayProxy;
-use Symfony\Component\VarExporter\LazyProxyTrait;
 use Symfony\Component\VarExporter\ProxyHelper;
 
 class RedisProxiesTest extends TestCase
@@ -34,8 +35,8 @@ public function testRedisProxy($class)
         $expected = substr($proxy, 0, 2 + strpos($proxy, '}'));
         $methods = [];
 
-        foreach ((new \ReflectionClass(sprintf('Symfony\Component\Cache\Traits\\%s%dProxy', $class, $version)))->getMethods() as $method) {
-            if ('reset' === $method->name || method_exists(LazyProxyTrait::class, $method->name)) {
+        foreach ((new \ReflectionClass(\sprintf('Symfony\Component\Cache\Traits\\%s%dProxy', $class, $version)))->getMethods() as $method) {
+            if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name)) {
                 continue;
             }
             $return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
@@ -87,7 +88,7 @@ public function testRelayProxy()
         $expectedMethods = [];
 
         foreach ((new \ReflectionClass(RelayProxy::class))->getMethods() as $method) {
-            if ('reset' === $method->name || method_exists(LazyProxyTrait::class, $method->name) || $method->isStatic()) {
+            if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
                 continue;
             }
 
@@ -121,4 +122,51 @@ public function testRelayProxy()
 
         $this->assertEquals($expectedProxy, $proxy);
     }
+
+    /**
+     * @requires extension relay
+     */
+    public function testRelayClusterProxy()
+    {
+        $proxy = file_get_contents(\dirname(__DIR__, 2).'/Traits/RelayClusterProxy.php');
+        $proxy = substr($proxy, 0, 2 + strpos($proxy, '}'));
+        $expectedProxy = $proxy;
+        $methods = [];
+        $expectedMethods = [];
+
+        foreach ((new \ReflectionClass(RelayClusterProxy::class))->getMethods() as $method) {
+            if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
+                continue;
+            }
+
+            $return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
+            $expectedMethods[$method->name] = "\n    ".ProxyHelper::exportSignature($method, false, $args)."\n".<<<EOPHP
+                {
+                    {$return}\$this->initializeLazyObject()->{$method->name}({$args});
+                }
+
+            EOPHP;
+        }
+
+        foreach ((new \ReflectionClass(RelayCluster::class))->getMethods() as $method) {
+            if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
+                continue;
+            }
+            $return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
+            $methods[$method->name] = "\n    ".ProxyHelper::exportSignature($method, false, $args)."\n".<<<EOPHP
+                {
+                    {$return}\$this->initializeLazyObject()->{$method->name}({$args});
+                }
+
+            EOPHP;
+        }
+
+        uksort($methods, 'strnatcmp');
+        $proxy .= implode('', $methods)."}\n";
+
+        uksort($expectedMethods, 'strnatcmp');
+        $expectedProxy .= implode('', $expectedMethods)."}\n";
+
+        $this->assertEquals($expectedProxy, $proxy);
+    }
 }
diff --git a/Traits/AbstractAdapterTrait.php b/Traits/AbstractAdapterTrait.php
index 6a716743..ac8dc97a 100644
--- a/Traits/AbstractAdapterTrait.php
+++ b/Traits/AbstractAdapterTrait.php
@@ -35,6 +35,7 @@ trait AbstractAdapterTrait
      */
     private static \Closure $mergeByLifetime;
 
+    private readonly string $rootNamespace;
     private string $namespace = '';
     private int $defaultLifetime;
     private string $namespaceVersion = '';
@@ -106,15 +107,16 @@ public function clear(string $prefix = ''): bool
     {
         $this->deferred = [];
         if ($cleared = $this->versioningIsEnabled) {
+            $rootNamespace = $this->rootNamespace ??= $this->namespace;
             if ('' === $namespaceVersionToClear = $this->namespaceVersion) {
-                foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
+                foreach ($this->doFetch([static::NS_SEPARATOR.$rootNamespace]) as $v) {
                     $namespaceVersionToClear = $v;
                 }
             }
-            $namespaceToClear = $this->namespace.$namespaceVersionToClear;
+            $namespaceToClear = $rootNamespace.$namespaceVersionToClear;
             $namespaceVersion = self::formatNamespaceVersion(mt_rand());
             try {
-                $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0);
+                $e = $this->doSave([static::NS_SEPARATOR.$rootNamespace => $namespaceVersion], 0);
             } catch (\Exception $e) {
             }
             if (true !== $e && [] !== $e) {
@@ -247,6 +249,16 @@ public function saveDeferred(CacheItemInterface $item): bool
         return true;
     }
 
+    public function withSubNamespace(string $namespace): static
+    {
+        $this->rootNamespace ??= $this->namespace;
+
+        $clone = clone $this;
+        $clone->namespace .= CacheItem::validateKey($namespace).static::NS_SEPARATOR;
+
+        return $clone;
+    }
+
     /**
      * Enables/disables versioning of items.
      *
@@ -318,19 +330,24 @@ private function generateItems(iterable $items, array &$keys): \Generator
     /**
      * @internal
      */
-    protected function getId(mixed $key): string
+    protected function getId(mixed $key, ?string $namespace = null): string
     {
-        if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
+        $namespace ??= $this->namespace;
+
+        if ('' !== $this->namespaceVersion) {
+            $namespace .= $this->namespaceVersion;
+        } elseif ($this->versioningIsEnabled) {
+            $rootNamespace = $this->rootNamespace ??= $this->namespace;
             $this->ids = [];
             $this->namespaceVersion = '1'.static::NS_SEPARATOR;
             try {
-                foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
+                foreach ($this->doFetch([static::NS_SEPARATOR.$rootNamespace]) as $v) {
                     $this->namespaceVersion = $v;
                 }
                 $e = true;
                 if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) {
                     $this->namespaceVersion = self::formatNamespaceVersion(time());
-                    $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0);
+                    $e = $this->doSave([static::NS_SEPARATOR.$rootNamespace => $this->namespaceVersion], 0);
                 }
             } catch (\Exception $e) {
             }
@@ -338,25 +355,34 @@ protected function getId(mixed $key): string
                 $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
                 CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
             }
+
+            $namespace .= $this->namespaceVersion;
         }
 
         if (\is_string($key) && isset($this->ids[$key])) {
-            return $this->namespace.$this->namespaceVersion.$this->ids[$key];
-        }
-        \assert('' !== CacheItem::validateKey($key));
-        $this->ids[$key] = $key;
+            $id = $this->ids[$key];
+        } else {
+            \assert('' !== CacheItem::validateKey($key));
+            $this->ids[$key] = $key;
 
-        if (\count($this->ids) > 1000) {
-            $this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys
-        }
+            if (\count($this->ids) > 1000) {
+                $this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys
+            }
+
+            if (null === $this->maxIdLength) {
+                return $namespace.$key;
+            }
+            if (\strlen($id = $namespace.$key) <= $this->maxIdLength) {
+                return $id;
+            }
 
-        if (null === $this->maxIdLength) {
-            return $this->namespace.$this->namespaceVersion.$key;
-        }
-        if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
             // Use xxh128 to favor speed over security, which is not an issue here
             $this->ids[$key] = $id = substr_replace(base64_encode(hash('xxh128', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2));
-            $id = $this->namespace.$this->namespaceVersion.$id;
+        }
+        $id = $namespace.$id;
+
+        if (null !== $this->maxIdLength && \strlen($id) > $this->maxIdLength) {
+            return base64_encode(hash('xxh128', $id, true));
         }
 
         return $id;
diff --git a/Traits/RedisTrait.php b/Traits/RedisTrait.php
index f6bb9bbe..19b4b911 100644
--- a/Traits/RedisTrait.php
+++ b/Traits/RedisTrait.php
@@ -20,6 +20,7 @@
 use Predis\Connection\Replication\ReplicationInterface as Predis2ReplicationInterface;
 use Predis\Response\ErrorInterface;
 use Predis\Response\Status;
+use Relay\Cluster as RelayCluster;
 use Relay\Relay;
 use Relay\Sentinel;
 use Symfony\Component\Cache\Exception\CacheException;
@@ -37,23 +38,25 @@ trait RedisTrait
 {
     private static array $defaultConnectionOptions = [
         'class' => null,
-        'persistent' => 0,
+        'persistent' => false,
         'persistent_id' => null,
         'timeout' => 30,
         'read_timeout' => 0,
         'retry_interval' => 0,
         'tcp_keepalive' => 0,
         'lazy' => null,
-        'redis_cluster' => false,
-        'redis_sentinel' => null,
+        'cluster' => false,
+        'cluster_command_timeout' => 0,
+        'cluster_relay_context' => [],
+        'sentinel' => null,
         'dbindex' => 0,
         'failover' => 'none',
         'ssl' => null, // see https://php.net/context.ssl
     ];
-    private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
+    private \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
     private MarshallerInterface $marshaller;
 
-    private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
+    private function init(\Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
     {
         parent::__construct($namespace, $defaultLifetime);
 
@@ -85,15 +88,15 @@ private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInter
      *
      * @throws InvalidArgumentException when the DSN is invalid
      */
-    public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay
+    public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay|RelayCluster
     {
-        if (str_starts_with($dsn, 'redis:')) {
-            $scheme = 'redis';
-        } elseif (str_starts_with($dsn, 'rediss:')) {
-            $scheme = 'rediss';
-        } else {
-            throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');
-        }
+        $scheme = match (true) {
+            str_starts_with($dsn, 'redis:') => 'redis',
+            str_starts_with($dsn, 'rediss:') => 'rediss',
+            str_starts_with($dsn, 'valkey:') => 'valkey',
+            str_starts_with($dsn, 'valkeys:') => 'valkeys',
+            default => throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:" nor "valkey[s]:".'),
+        };
 
         if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
             throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');
@@ -121,7 +124,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
 
         $query = $hosts = [];
 
-        $tls = 'rediss' === $scheme;
+        $tls = 'rediss' === $scheme || 'valkeys' === $scheme;
         $tcpScheme = $tls ? 'tls' : 'tcp';
 
         if (isset($params['query'])) {
@@ -174,28 +177,42 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
 
         $params += $query + $options + self::$defaultConnectionOptions;
 
-        if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) {
-            throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.');
+        $aliases = [
+            'sentinel_master' => 'sentinel',
+            'redis_sentinel' => 'sentinel',
+            'redis_cluster' => 'cluster',
+        ];
+        foreach ($aliases as $alias => $key) {
+            $params[$key] = match (true) {
+                \array_key_exists($key, $query) => $query[$key],
+                \array_key_exists($alias, $query) => $query[$alias],
+                \array_key_exists($key, $options) => $options[$key],
+                \array_key_exists($alias, $options) => $options[$alias],
+                default => $params[$key],
+            };
         }
 
-        $params['redis_sentinel'] ??= $params['sentinel_master'] ?? null;
-
-        if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
+        if (isset($params['sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
             throw new CacheException('Redis Sentinel support requires one of: "predis/predis", "ext-redis >= 5.2", "ext-relay".');
         }
 
-        if (isset($params['lazy'])) {
-            $params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
+        foreach (['lazy', 'persistent', 'cluster'] as $option) {
+            if (!\is_bool($params[$option] ?? false)) {
+                $params[$option] = filter_var($params[$option], \FILTER_VALIDATE_BOOLEAN);
+            }
         }
-        $params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
 
-        if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
-            throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
+        if ($params['cluster'] && isset($params['sentinel'])) {
+            throw new InvalidArgumentException('Cannot use both "cluster" and "sentinel" at the same time.');
         }
 
         $class = $params['class'] ?? match (true) {
-            $params['redis_cluster'] => \extension_loaded('redis') ? \RedisCluster::class : \Predis\Client::class,
-            isset($params['redis_sentinel']) => match (true) {
+            $params['cluster'] => match (true) {
+                \extension_loaded('redis') => \RedisCluster::class,
+                \extension_loaded('relay') => RelayCluster::class,
+                default => \Predis\Client::class,
+            },
+            isset($params['sentinel']) => match (true) {
                 \extension_loaded('redis') => \Redis::class,
                 \extension_loaded('relay') => Relay::class,
                 default => \Predis\Client::class,
@@ -206,7 +223,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
             default => \Predis\Client::class,
         };
 
-        if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
+        if (isset($params['sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
             throw new CacheException(\sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and neither ext-redis >= 5.2 nor ext-relay have been found.', $class));
         }
 
@@ -230,7 +247,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
                         $host = 'tls://'.$host;
                     }
 
-                    if (!isset($params['redis_sentinel'])) {
+                    if (!isset($params['sentinel'])) {
                         break;
                     }
 
@@ -256,37 +273,22 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
                             $sentinel = @new $sentinelClass($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
                         }
 
-                        if ($address = @$sentinel->getMasterAddrByName($params['redis_sentinel'])) {
+                        if ($address = @$sentinel->getMasterAddrByName($params['sentinel'])) {
                             [$host, $port] = $address;
                         }
                     } catch (\RedisException|\Relay\Exception $redisException) {
                     }
                 } while (++$hostIndex < \count($hosts) && !$address);
 
-                if (isset($params['redis_sentinel']) && !$address) {
-                    throw new InvalidArgumentException(\sprintf('Failed to retrieve master information from sentinel "%s".', $params['redis_sentinel']), previous: $redisException ?? null);
+                if (isset($params['sentinel']) && !$address) {
+                    throw new InvalidArgumentException(\sprintf('Failed to retrieve master information from sentinel "%s".', $params['sentinel']), previous: $redisException ?? null);
                 }
 
                 try {
                     $extra = [
-                        'stream' => $params['ssl'] ?? null,
-                    ];
-                    $booleanStreamOptions = [
-                        'allow_self_signed',
-                        'capture_peer_cert',
-                        'capture_peer_cert_chain',
-                        'disable_compression',
-                        'SNI_enabled',
-                        'verify_peer',
-                        'verify_peer_name',
+                        'stream' => self::filterSslOptions($params['ssl'] ?? []) ?: null,
                     ];
 
-                    foreach ($extra['stream'] ?? [] as $streamOption => $value) {
-                        if (\in_array($streamOption, $booleanStreamOptions, true) && \is_string($value)) {
-                            $extra['stream'][$streamOption] = filter_var($value, \FILTER_VALIDATE_BOOL);
-                        }
-                    }
-
                     if (isset($params['auth'])) {
                         $extra['auth'] = $params['auth'];
                     }
@@ -348,6 +350,59 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
             if (0 < $params['tcp_keepalive'] && (!$isRedisExt || \defined('Redis::OPT_TCP_KEEPALIVE'))) {
                 $redis->setOption($isRedisExt ? \Redis::OPT_TCP_KEEPALIVE : Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
             }
+        } elseif (is_a($class, RelayCluster::class, true)) {
+            if (version_compare(phpversion('relay'), '0.10.0', '<')) {
+                throw new InvalidArgumentException('Using RelayCluster is supported from ext-relay 0.10.0 or higher.');
+            }
+
+            $initializer = static function () use ($class, $params, $hosts) {
+                foreach ($hosts as $i => $host) {
+                    $hosts[$i] = match ($host['scheme']) {
+                        'tcp' => $host['host'].':'.$host['port'],
+                        'tls' => 'tls://'.$host['host'].':'.$host['port'],
+                        default => $host['path'],
+                    };
+                }
+
+                try {
+                    $context = $params['cluster_relay_context'];
+                    $context['stream'] = self::filterSslOptions($params['ssl'] ?? []) ?: null;
+
+                    foreach ($context as $name => $value) {
+                        match ($name) {
+                            'use-cache', 'client-tracking', 'throw-on-error', 'client-invalidations', 'reply-literal', 'persistent',
+                                => $context[$name] = filter_var($value, \FILTER_VALIDATE_BOOLEAN),
+                            'max-retries', 'serializer', 'compression', 'compression-level',
+                                => $context[$name] = filter_var($value, \FILTER_VALIDATE_INT),
+                            default => null,
+                        };
+                    }
+
+                    $relayCluster = new $class(
+                        name: null,
+                        seeds: $hosts,
+                        connect_timeout: $params['timeout'],
+                        command_timeout: $params['cluster_command_timeout'],
+                        persistent: $params['persistent'],
+                        auth: $params['auth'] ?? null,
+                        context: $context,
+                    );
+                } catch (\Relay\Exception $e) {
+                    throw new InvalidArgumentException('Relay cluster connection failed: '.$e->getMessage());
+                }
+
+                if (0 < $params['tcp_keepalive']) {
+                    $relayCluster->setOption(Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
+                }
+
+                if (0 < $params['read_timeout']) {
+                    $relayCluster->setOption(Relay::OPT_READ_TIMEOUT, $params['read_timeout']);
+                }
+
+                return $relayCluster;
+            };
+
+            $redis = $params['lazy'] ? RelayClusterProxy::createLazyProxy($initializer) : $initializer();
         } elseif (is_a($class, \RedisCluster::class, true)) {
             $initializer = static function () use ($isRedisExt, $class, $params, $hosts) {
                 foreach ($hosts as $i => $host) {
@@ -359,7 +414,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
                 }
 
                 try {
-                    $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
+                    $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
                 } catch (\RedisClusterException $e) {
                     throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
                 }
@@ -379,11 +434,14 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
 
             $redis = $params['lazy'] ? RedisClusterProxy::createLazyProxy($initializer) : $initializer();
         } elseif (is_a($class, \Predis\ClientInterface::class, true)) {
-            if ($params['redis_cluster']) {
+            if ($params['cluster']) {
                 $params['cluster'] = 'redis';
-            } elseif (isset($params['redis_sentinel'])) {
+            } else {
+                unset($params['cluster']);
+            }
+            if (isset($params['sentinel'])) {
                 $params['replication'] = 'sentinel';
-                $params['service'] = $params['redis_sentinel'];
+                $params['service'] = $params['sentinel'];
             }
             $params += ['parameters' => []];
             $params['parameters'] += [
@@ -411,7 +469,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
                 }
             }
 
-            if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
+            if (1 === \count($hosts) && !isset($params['cluster']) & !isset($params['sentinel'])) {
                 $hosts = $hosts[0];
             } elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) {
                 $params['replication'] = true;
@@ -419,8 +477,8 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
             }
             $params['exceptions'] = false;
 
-            $redis = new $class($hosts, array_diff_key($params, self::$defaultConnectionOptions));
-            if (isset($params['redis_sentinel'])) {
+            $redis = new $class($hosts, array_diff_key($params, array_diff_key(self::$defaultConnectionOptions, ['cluster' => null])));
+            if (isset($params['sentinel'])) {
                 $redis->getConnection()->setSentinelTimeout($params['timeout']);
             }
         } elseif (class_exists($class, false)) {
@@ -478,6 +536,35 @@ protected function doClear(string $namespace): bool
         }
 
         $cleared = true;
+
+        if ($this->redis instanceof RelayCluster) {
+            $prefix = Relay::SCAN_PREFIX & $this->redis->getOption(Relay::OPT_SCAN) ? '' : $this->redis->getOption(Relay::OPT_PREFIX);
+            $prefixLen = \strlen($prefix);
+            $pattern = $prefix.$namespace.'*';
+            foreach ($this->redis->_masters() as $ipAndPort) {
+                $address = implode(':', $ipAndPort);
+                $cursor = null;
+                do {
+                    $keys = $this->redis->scan($cursor, $address, $pattern, 1000);
+                    if (isset($keys[1]) && \is_array($keys[1])) {
+                        $cursor = $keys[0];
+                        $keys = $keys[1];
+                    }
+
+                    if ($keys) {
+                        if ($prefixLen) {
+                            foreach ($keys as $i => $key) {
+                                $keys[$i] = substr($key, $prefixLen);
+                            }
+                        }
+                        $this->doDelete($keys);
+                    }
+                } while ($cursor);
+            }
+
+            return $cleared;
+        }
+
         $hosts = $this->getHosts();
         $host = reset($hosts);
         if ($host instanceof \Predis\Client) {
@@ -605,8 +692,9 @@ private function pipeline(\Closure $generator, ?object $redis = null): \Generato
         $ids = [];
         $redis ??= $this->redis;
 
-        if ($redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
+        if ($redis instanceof \RedisCluster || $redis instanceof RelayCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
             // phpredis & predis don't support pipelining with RedisCluster
+            // \Relay\Cluster does not support multi with pipeline mode
             // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
             // see https://github.com/nrk/predis/issues/267#issuecomment-123781423
             $results = [];
@@ -687,4 +775,17 @@ private function getHosts(): array
 
         return $hosts;
     }
+
+    private static function filterSslOptions(array $options): array
+    {
+        foreach ($options as $name => $value) {
+            match ($name) {
+                'allow_self_signed', 'capture_peer_cert', 'capture_peer_cert_chain', 'disable_compression', 'SNI_enabled', 'verify_peer', 'verify_peer_name',
+                    => $options[$name] = filter_var($value, \FILTER_VALIDATE_BOOLEAN),
+                default => null,
+            };
+        }
+
+        return $options;
+    }
 }
diff --git a/Traits/RelayClusterProxy.php b/Traits/RelayClusterProxy.php
new file mode 100644
index 00000000..fd5f08b5
--- /dev/null
+++ b/Traits/RelayClusterProxy.php
@@ -0,0 +1,1204 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Relay\Cluster;
+use Relay\Relay;
+use Symfony\Component\VarExporter\LazyObjectInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
+class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
+class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
+
+/**
+ * @internal
+ */
+class RelayClusterProxy extends Cluster implements ResetInterface, LazyObjectInterface
+{
+    use RedisProxyTrait {
+        resetLazyObject as reset;
+    }
+
+    public function __construct(
+        string|null $name,
+        array|null $seeds = null,
+        int|float $connect_timeout = 0,
+        int|float $command_timeout = 0,
+        bool $persistent = false,
+        #[\SensitiveParameter] mixed $auth = null,
+        array|null $context = null,
+    ) {
+        $this->initializeLazyObject()->__construct(...\func_get_args());
+    }
+
+    public function close(): bool
+    {
+        return $this->initializeLazyObject()->close(...\func_get_args());
+    }
+
+    public function listen(?callable $callback): bool
+    {
+        return $this->initializeLazyObject()->listen(...\func_get_args());
+    }
+
+    public function onFlushed(?callable $callback): bool
+    {
+        return $this->initializeLazyObject()->onFlushed(...\func_get_args());
+    }
+
+    public function onInvalidated(?callable $callback, ?string $pattern = null): bool
+    {
+        return $this->initializeLazyObject()->onInvalidated(...\func_get_args());
+    }
+
+    public function dispatchEvents(): false|int
+    {
+        return $this->initializeLazyObject()->dispatchEvents(...\func_get_args());
+    }
+
+    public function dump(mixed $key): Cluster|string|false
+    {
+        return $this->initializeLazyObject()->dump(...\func_get_args());
+    }
+
+    public function getOption(int $option): mixed
+    {
+        return $this->initializeLazyObject()->getOption(...\func_get_args());
+    }
+
+    public function setOption(int $option, mixed $value): bool
+    {
+        return $this->initializeLazyObject()->setOption(...\func_get_args());
+    }
+
+    public function getTransferredBytes(): array|false
+    {
+        return $this->initializeLazyObject()->getTransferredBytes(...\func_get_args());
+    }
+
+    public function getrange(mixed $key, int $start, int $end): Cluster|string|false
+    {
+        return $this->initializeLazyObject()->getrange(...\func_get_args());
+    }
+
+    public function addIgnorePatterns(string ...$pattern): int
+    {
+        return $this->initializeLazyObject()->addIgnorePatterns(...\func_get_args());
+    }
+
+    public function addAllowPatterns(string ...$pattern): int
+    {
+        return $this->initializeLazyObject()->addAllowPatterns(...\func_get_args());
+    }
+
+    public function _serialize(mixed $value): string
+    {
+        return $this->initializeLazyObject()->_serialize(...\func_get_args());
+    }
+
+    public function _unserialize(string $value): mixed
+    {
+        return $this->initializeLazyObject()->_unserialize(...\func_get_args());
+    }
+
+    public function _compress(string $value): string
+    {
+        return $this->initializeLazyObject()->_compress(...\func_get_args());
+    }
+
+    public function _uncompress(string $value): string
+    {
+        return $this->initializeLazyObject()->_uncompress(...\func_get_args());
+    }
+
+    public function _pack(mixed $value): string
+    {
+        return $this->initializeLazyObject()->_pack(...\func_get_args());
+    }
+
+    public function _unpack(string $value): mixed
+    {
+        return $this->initializeLazyObject()->_unpack(...\func_get_args());
+    }
+
+    public function _prefix(mixed $value): string
+    {
+        return $this->initializeLazyObject()->_prefix(...\func_get_args());
+    }
+
+    public function getLastError(): ?string
+    {
+        return $this->initializeLazyObject()->getLastError(...\func_get_args());
+    }
+
+    public function clearLastError(): bool
+    {
+        return $this->initializeLazyObject()->clearLastError(...\func_get_args());
+    }
+
+    public function clearTransferredBytes(): bool
+    {
+        return $this->initializeLazyObject()->clearTransferredBytes(...\func_get_args());
+    }
+
+    public function endpointId(): array|false
+    {
+        return $this->initializeLazyObject()->endpointId(...\func_get_args());
+    }
+
+    public function rawCommand(array|string $key_or_address, string $cmd, mixed ...$args): mixed
+    {
+        return $this->initializeLazyObject()->rawCommand(...\func_get_args());
+    }
+
+    public function cluster(array|string $key_or_address, string $operation, mixed ...$args): mixed
+    {
+        return $this->initializeLazyObject()->cluster(...\func_get_args());
+    }
+
+    public function info(array|string $key_or_address, string ...$sections): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->info(...\func_get_args());
+    }
+
+    public function flushdb(array|string $key_or_address, bool|null $sync = null): Cluster|bool
+    {
+        return $this->initializeLazyObject()->flushdb(...\func_get_args());
+    }
+
+    public function flushall(array|string $key_or_address, bool|null $sync = null): Cluster|bool
+    {
+        return $this->initializeLazyObject()->flushall(...\func_get_args());
+    }
+
+    public function dbsize(array|string $key_or_address): Cluster|int|false
+    {
+        return $this->initializeLazyObject()->dbsize(...\func_get_args());
+    }
+
+    public function waitaof(array|string $key_or_address, int $numlocal, int $numremote, int $timeout): Relay|array|false
+    {
+        return $this->initializeLazyObject()->waitaof(...\func_get_args());
+    }
+
+    public function restore(mixed $key, int $ttl, string $value, array|null $options = null): Cluster|bool
+    {
+        return $this->initializeLazyObject()->restore(...\func_get_args());
+    }
+
+    public function echo(array|string $key_or_address, string $message): Cluster|string|false
+    {
+        return $this->initializeLazyObject()->echo(...\func_get_args());
+    }
+
+    public function ping(array|string $key_or_address, string|null $message = null): Cluster|bool|string
+    {
+        return $this->initializeLazyObject()->ping(...\func_get_args());
+    }
+
+    public function idleTime(): int
+    {
+        return $this->initializeLazyObject()->idleTime(...\func_get_args());
+    }
+
+    public function randomkey(array|string $key_or_address): Cluster|bool|string
+    {
+        return $this->initializeLazyObject()->randomkey(...\func_get_args());
+    }
+
+    public function time(array|string $key_or_address): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->time(...\func_get_args());
+    }
+
+    public function bgrewriteaof(array|string $key_or_address): Cluster|bool
+    {
+        return $this->initializeLazyObject()->bgrewriteaof(...\func_get_args());
+    }
+
+    public function lastsave(array|string $key_or_address): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->lastsave(...\func_get_args());
+    }
+
+    public function lcs(mixed $key1, mixed $key2, array|null $options = null): mixed
+    {
+        return $this->initializeLazyObject()->lcs(...\func_get_args());
+    }
+
+    public function bgsave(array|string $key_or_address, bool $schedule = false): Cluster|bool
+    {
+        return $this->initializeLazyObject()->bgsave(...\func_get_args());
+    }
+
+    public function save(array|string $key_or_address): Cluster|bool
+    {
+        return $this->initializeLazyObject()->save(...\func_get_args());
+    }
+
+    public function role(array|string $key_or_address): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->role(...\func_get_args());
+    }
+
+    public function ttl(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->ttl(...\func_get_args());
+    }
+
+    public function pttl(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->pttl(...\func_get_args());
+    }
+
+    public function exists(mixed ...$keys): Cluster|bool|int
+    {
+        return $this->initializeLazyObject()->exists(...\func_get_args());
+    }
+
+    public function eval(mixed $script, array $args = [], int $num_keys = 0): mixed
+    {
+        return $this->initializeLazyObject()->eval(...\func_get_args());
+    }
+
+    public function eval_ro(mixed $script, array $args = [], int $num_keys = 0): mixed
+    {
+        return $this->initializeLazyObject()->eval_ro(...\func_get_args());
+    }
+
+    public function evalsha(string $sha, array $args = [], int $num_keys = 0): mixed
+    {
+        return $this->initializeLazyObject()->evalsha(...\func_get_args());
+    }
+
+    public function evalsha_ro(string $sha, array $args = [], int $num_keys = 0): mixed
+    {
+        return $this->initializeLazyObject()->evalsha_ro(...\func_get_args());
+    }
+
+    public function client(array|string $key_or_address, string $operation, mixed ...$args): mixed
+    {
+        return $this->initializeLazyObject()->client(...\func_get_args());
+    }
+
+    public function geoadd(mixed $key, float $lng, float $lat, string $member, mixed ...$other_triples_and_options): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->geoadd(...\func_get_args());
+    }
+
+    public function geodist(mixed $key, string $src, string $dst, string|null $unit = null): Cluster|float|false
+    {
+        return $this->initializeLazyObject()->geodist(...\func_get_args());
+    }
+
+    public function geohash(mixed $key, string $member, string ...$other_members): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->geohash(...\func_get_args());
+    }
+
+    public function georadius(mixed $key, float $lng, float $lat, float $radius, string $unit, array $options = []): mixed
+    {
+        return $this->initializeLazyObject()->georadius(...\func_get_args());
+    }
+
+    public function georadiusbymember(mixed $key, string $member, float $radius, string $unit, array $options = []): mixed
+    {
+        return $this->initializeLazyObject()->georadiusbymember(...\func_get_args());
+    }
+
+    public function georadiusbymember_ro(mixed $key, string $member, float $radius, string $unit, array $options = []): mixed
+    {
+        return $this->initializeLazyObject()->georadiusbymember_ro(...\func_get_args());
+    }
+
+    public function georadius_ro(mixed $key, float $lng, float $lat, float $radius, string $unit, array $options = []): mixed
+    {
+        return $this->initializeLazyObject()->georadius_ro(...\func_get_args());
+    }
+
+    public function geosearchstore(mixed $dstkey, mixed $srckey, array|string $position, array|int|float $shape, string $unit, array $options = []): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->geosearchstore(...\func_get_args());
+    }
+
+    public function geosearch(mixed $key, array|string $position, array|int|float $shape, string $unit, array $options = []): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->geosearch(...\func_get_args());
+    }
+
+    public function get(mixed $key): mixed
+    {
+        return $this->initializeLazyObject()->get(...\func_get_args());
+    }
+
+    public function getset(mixed $key, mixed $value): mixed
+    {
+        return $this->initializeLazyObject()->getset(...\func_get_args());
+    }
+
+    public function setrange(mixed $key, int $start, mixed $value): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->setrange(...\func_get_args());
+    }
+
+    public function getbit(mixed $key, int $pos): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->getbit(...\func_get_args());
+    }
+
+    public function bitcount(mixed $key, int $start = 0, int $end = -1, bool $by_bit = false): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->bitcount(...\func_get_args());
+    }
+
+    public function config(array|string $key_or_address, string $operation, mixed ...$args): mixed
+    {
+        return $this->initializeLazyObject()->config(...\func_get_args());
+    }
+
+    public function command(mixed ...$args): Cluster|array|false|int
+    {
+        return $this->initializeLazyObject()->command(...\func_get_args());
+    }
+
+    public function bitop(string $operation, string $dstkey, string $srckey, string ...$other_keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->bitop(...\func_get_args());
+    }
+
+    public function bitpos(mixed $key, int $bit, ?int $start = null, ?int $end = null, bool $by_bit = false): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->bitpos(...\func_get_args());
+    }
+
+    public function blmove(mixed $srckey, mixed $dstkey, string $srcpos, string $dstpos, float $timeout): Cluster|string|false|null
+    {
+        return $this->initializeLazyObject()->blmove(...\func_get_args());
+    }
+
+    public function lmove(mixed $srckey, mixed $dstkey, string $srcpos, string $dstpos): Cluster|string|false|null
+    {
+        return $this->initializeLazyObject()->lmove(...\func_get_args());
+    }
+
+    public function setbit(mixed $key, int $pos, int $value): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->setbit(...\func_get_args());
+    }
+
+    public function acl(array|string $key_or_address, string $operation, string ...$args): mixed
+    {
+        return $this->initializeLazyObject()->acl(...\func_get_args());
+    }
+
+    public function append(mixed $key, mixed $value): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->append(...\func_get_args());
+    }
+
+    public function set(mixed $key, mixed $value, mixed $options = null): Cluster|string|bool
+    {
+        return $this->initializeLazyObject()->set(...\func_get_args());
+    }
+
+    public function getex(mixed $key, ?array $options = null): mixed
+    {
+        return $this->initializeLazyObject()->getex(...\func_get_args());
+    }
+
+    public function setex(mixed $key, int $seconds, mixed $value): Cluster|bool
+    {
+        return $this->initializeLazyObject()->setex(...\func_get_args());
+    }
+
+    public function pfadd(mixed $key, array $elements): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->pfadd(...\func_get_args());
+    }
+
+    public function pfcount(mixed $key): Cluster|int|false
+    {
+        return $this->initializeLazyObject()->pfcount(...\func_get_args());
+    }
+
+    public function pfmerge(string $dstkey, array $srckeys): Cluster|bool
+    {
+        return $this->initializeLazyObject()->pfmerge(...\func_get_args());
+    }
+
+    public function psetex(mixed $key, int $milliseconds, mixed $value): Cluster|bool
+    {
+        return $this->initializeLazyObject()->psetex(...\func_get_args());
+    }
+
+    public function publish(string $channel, string $message): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->publish(...\func_get_args());
+    }
+
+    public function pubsub(array|string $key_or_address, string $operation, mixed ...$args): mixed
+    {
+        return $this->initializeLazyObject()->pubsub(...\func_get_args());
+    }
+
+    public function setnx(mixed $key, mixed $value): Cluster|bool
+    {
+        return $this->initializeLazyObject()->setnx(...\func_get_args());
+    }
+
+    public function mget(array $keys): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->mget(...\func_get_args());
+    }
+
+    public function mset(array $kvals): Cluster|array|bool
+    {
+        return $this->initializeLazyObject()->mset(...\func_get_args());
+    }
+
+    public function msetnx(array $kvals): Cluster|array|bool
+    {
+        return $this->initializeLazyObject()->msetnx(...\func_get_args());
+    }
+
+    public function rename(mixed $key, mixed $newkey): Cluster|bool
+    {
+        return $this->initializeLazyObject()->rename(...\func_get_args());
+    }
+
+    public function renamenx(mixed $key, mixed $newkey): Cluster|bool
+    {
+        return $this->initializeLazyObject()->renamenx(...\func_get_args());
+    }
+
+    public function del(mixed ...$keys): Cluster|bool|int
+    {
+        return $this->initializeLazyObject()->del(...\func_get_args());
+    }
+
+    public function unlink(mixed ...$keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->unlink(...\func_get_args());
+    }
+
+    public function expire(mixed $key, int $seconds, string|null $mode = null): Cluster|bool
+    {
+        return $this->initializeLazyObject()->expire(...\func_get_args());
+    }
+
+    public function pexpire(mixed $key, int $milliseconds): Cluster|bool
+    {
+        return $this->initializeLazyObject()->pexpire(...\func_get_args());
+    }
+
+    public function expireat(mixed $key, int $timestamp): Cluster|bool
+    {
+        return $this->initializeLazyObject()->expireat(...\func_get_args());
+    }
+
+    public function expiretime(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->expiretime(...\func_get_args());
+    }
+
+    public function pexpireat(mixed $key, int $timestamp_ms): Cluster|bool
+    {
+        return $this->initializeLazyObject()->pexpireat(...\func_get_args());
+    }
+
+    public static function flushMemory(?string $endpointId = null, ?int $db = null): bool
+    {
+        return Cluster::flushMemory(...\func_get_args());
+    }
+
+    public function pexpiretime(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->pexpiretime(...\func_get_args());
+    }
+
+    public function persist(mixed $key): Cluster|bool
+    {
+        return $this->initializeLazyObject()->persist(...\func_get_args());
+    }
+
+    public function type(mixed $key): Cluster|bool|int|string
+    {
+        return $this->initializeLazyObject()->type(...\func_get_args());
+    }
+
+    public function lrange(mixed $key, int $start, int $stop): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->lrange(...\func_get_args());
+    }
+
+    public function lpush(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->lpush(...\func_get_args());
+    }
+
+    public function rpush(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->rpush(...\func_get_args());
+    }
+
+    public function lpushx(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->lpushx(...\func_get_args());
+    }
+
+    public function rpushx(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->rpushx(...\func_get_args());
+    }
+
+    public function lset(mixed $key, int $index, mixed $member): Cluster|bool
+    {
+        return $this->initializeLazyObject()->lset(...\func_get_args());
+    }
+
+    public function lpop(mixed $key, int $count = 1): mixed
+    {
+        return $this->initializeLazyObject()->lpop(...\func_get_args());
+    }
+
+    public function lpos(mixed $key, mixed $value, array|null $options = null): mixed
+    {
+        return $this->initializeLazyObject()->lpos(...\func_get_args());
+    }
+
+    public function rpop(mixed $key, int $count = 1): mixed
+    {
+        return $this->initializeLazyObject()->rpop(...\func_get_args());
+    }
+
+    public function rpoplpush(mixed $srckey, mixed $dstkey): mixed
+    {
+        return $this->initializeLazyObject()->rpoplpush(...\func_get_args());
+    }
+
+    public function brpoplpush(mixed $srckey, mixed $dstkey, float $timeout): mixed
+    {
+        return $this->initializeLazyObject()->brpoplpush(...\func_get_args());
+    }
+
+    public function blpop(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+    {
+        return $this->initializeLazyObject()->blpop(...\func_get_args());
+    }
+
+    public function blmpop(float $timeout, array $keys, string $from, int $count = 1): mixed
+    {
+        return $this->initializeLazyObject()->blmpop(...\func_get_args());
+    }
+
+    public function bzmpop(float $timeout, array $keys, string $from, int $count = 1): Cluster|array|false|null
+    {
+        return $this->initializeLazyObject()->bzmpop(...\func_get_args());
+    }
+
+    public function lmpop(array $keys, string $from, int $count = 1): mixed
+    {
+        return $this->initializeLazyObject()->lmpop(...\func_get_args());
+    }
+
+    public function zmpop(array $keys, string $from, int $count = 1): Cluster|array|false|null
+    {
+        return $this->initializeLazyObject()->zmpop(...\func_get_args());
+    }
+
+    public function brpop(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+    {
+        return $this->initializeLazyObject()->brpop(...\func_get_args());
+    }
+
+    public function bzpopmax(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+    {
+        return $this->initializeLazyObject()->bzpopmax(...\func_get_args());
+    }
+
+    public function bzpopmin(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+    {
+        return $this->initializeLazyObject()->bzpopmin(...\func_get_args());
+    }
+
+    public function object(string $op, mixed $key): mixed
+    {
+        return $this->initializeLazyObject()->object(...\func_get_args());
+    }
+
+    public function geopos(mixed $key, mixed ...$members): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->geopos(...\func_get_args());
+    }
+
+    public function lrem(mixed $key, mixed $member, int $count = 0): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->lrem(...\func_get_args());
+    }
+
+    public function lindex(mixed $key, int $index): mixed
+    {
+        return $this->initializeLazyObject()->lindex(...\func_get_args());
+    }
+
+    public function linsert(mixed $key, string $op, mixed $pivot, mixed $element): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->linsert(...\func_get_args());
+    }
+
+    public function ltrim(mixed $key, int $start, int $end): Cluster|bool
+    {
+        return $this->initializeLazyObject()->ltrim(...\func_get_args());
+    }
+
+    public static function maxMemory(): int
+    {
+        return Cluster::maxMemory();
+    }
+
+    public function hget(mixed $key, mixed $member): mixed
+    {
+        return $this->initializeLazyObject()->hget(...\func_get_args());
+    }
+
+    public function hstrlen(mixed $key, mixed $member): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->hstrlen(...\func_get_args());
+    }
+
+    public function hgetall(mixed $key): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->hgetall(...\func_get_args());
+    }
+
+    public function hkeys(mixed $key): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->hkeys(...\func_get_args());
+    }
+
+    public function hvals(mixed $key): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->hvals(...\func_get_args());
+    }
+
+    public function hmget(mixed $key, array $members): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->hmget(...\func_get_args());
+    }
+
+    public function hmset(mixed $key, array $members): Cluster|bool
+    {
+        return $this->initializeLazyObject()->hmset(...\func_get_args());
+    }
+
+    public function hexists(mixed $key, mixed $member): Cluster|bool
+    {
+        return $this->initializeLazyObject()->hexists(...\func_get_args());
+    }
+
+    public function hrandfield(mixed $key, array|null $options = null): Cluster|array|string|false
+    {
+        return $this->initializeLazyObject()->hrandfield(...\func_get_args());
+    }
+
+    public function hsetnx(mixed $key, mixed $member, mixed $value): Cluster|bool
+    {
+        return $this->initializeLazyObject()->hsetnx(...\func_get_args());
+    }
+
+    public function hset(mixed $key, mixed ...$keys_and_vals): Cluster|int|false
+    {
+        return $this->initializeLazyObject()->hset(...\func_get_args());
+    }
+
+    public function hdel(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->hdel(...\func_get_args());
+    }
+
+    public function hincrby(mixed $key, mixed $member, int $value): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->hincrby(...\func_get_args());
+    }
+
+    public function hincrbyfloat(mixed $key, mixed $member, float $value): Cluster|bool|float
+    {
+        return $this->initializeLazyObject()->hincrbyfloat(...\func_get_args());
+    }
+
+    public function incr(mixed $key, int $by = 1): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->incr(...\func_get_args());
+    }
+
+    public function decr(mixed $key, int $by = 1): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->decr(...\func_get_args());
+    }
+
+    public function incrby(mixed $key, int $value): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->incrby(...\func_get_args());
+    }
+
+    public function decrby(mixed $key, int $value): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->decrby(...\func_get_args());
+    }
+
+    public function incrbyfloat(mixed $key, float $value): Cluster|false|float
+    {
+        return $this->initializeLazyObject()->incrbyfloat(...\func_get_args());
+    }
+
+    public function sdiff(mixed $key, mixed ...$other_keys): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->sdiff(...\func_get_args());
+    }
+
+    public function sdiffstore(mixed $key, mixed ...$other_keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->sdiffstore(...\func_get_args());
+    }
+
+    public function sinter(mixed $key, mixed ...$other_keys): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->sinter(...\func_get_args());
+    }
+
+    public function sintercard(array $keys, int $limit = -1): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->sintercard(...\func_get_args());
+    }
+
+    public function sinterstore(mixed $key, mixed ...$other_keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->sinterstore(...\func_get_args());
+    }
+
+    public function sunion(mixed $key, mixed ...$other_keys): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->sunion(...\func_get_args());
+    }
+
+    public function sunionstore(mixed $key, mixed ...$other_keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->sunionstore(...\func_get_args());
+    }
+
+    public function subscribe(array $channels, callable $callback): bool
+    {
+        return $this->initializeLazyObject()->subscribe(...\func_get_args());
+    }
+
+    public function unsubscribe(array $channels = []): bool
+    {
+        return $this->initializeLazyObject()->unsubscribe(...\func_get_args());
+    }
+
+    public function psubscribe(array $patterns, callable $callback): bool
+    {
+        return $this->initializeLazyObject()->psubscribe(...\func_get_args());
+    }
+
+    public function punsubscribe(array $patterns = []): bool
+    {
+        return $this->initializeLazyObject()->punsubscribe(...\func_get_args());
+    }
+
+    public function ssubscribe(array $channels, callable $callback): bool
+    {
+        return $this->initializeLazyObject()->ssubscribe(...\func_get_args());
+    }
+
+    public function sunsubscribe(array $channels = []): bool
+    {
+        return $this->initializeLazyObject()->sunsubscribe(...\func_get_args());
+    }
+
+    public function touch(array|string $key_or_array, mixed ...$more_keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->touch(...\func_get_args());
+    }
+
+    public function multi(int $mode = Relay::MULTI): Cluster|bool
+    {
+        return $this->initializeLazyObject()->multi(...\func_get_args());
+    }
+
+    public function exec(): array|false
+    {
+        return $this->initializeLazyObject()->exec(...\func_get_args());
+    }
+
+    public function watch(mixed $key, mixed ...$other_keys): Cluster|bool
+    {
+        return $this->initializeLazyObject()->watch(...\func_get_args());
+    }
+
+    public function unwatch(): Cluster|bool
+    {
+        return $this->initializeLazyObject()->unwatch(...\func_get_args());
+    }
+
+    public function discard(): bool
+    {
+        return $this->initializeLazyObject()->discard(...\func_get_args());
+    }
+
+    public function getMode(bool $masked = false): int
+    {
+        return $this->initializeLazyObject()->getMode(...\func_get_args());
+    }
+
+    public function scan(mixed &$iterator, array|string $key_or_address, mixed $match = null, int $count = 0, string|null $type = null): array|false
+    {
+        return $this->initializeLazyObject()->scan($iterator, ...\array_slice(\func_get_args(), 1));
+    }
+
+    public function hscan(mixed $key, mixed &$iterator, mixed $match = null, int $count = 0): array|false
+    {
+        return $this->initializeLazyObject()->hscan($key, $iterator, ...\array_slice(\func_get_args(), 2));
+    }
+
+    public function sscan(mixed $key, mixed &$iterator, mixed $match = null, int $count = 0): array|false
+    {
+        return $this->initializeLazyObject()->sscan($key, $iterator, ...\array_slice(\func_get_args(), 2));
+    }
+
+    public function zscan(mixed $key, mixed &$iterator, mixed $match = null, int $count = 0): array|false
+    {
+        return $this->initializeLazyObject()->zscan($key, $iterator, ...\array_slice(\func_get_args(), 2));
+    }
+
+    public function zscore(mixed $key, mixed $member): Cluster|float|false
+    {
+        return $this->initializeLazyObject()->zscore(...\func_get_args());
+    }
+
+    public function keys(mixed $pattern): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->keys(...\func_get_args());
+    }
+
+    public function slowlog(array|string $key_or_address, string $operation, mixed ...$args): Cluster|array|bool|int
+    {
+        return $this->initializeLazyObject()->slowlog(...\func_get_args());
+    }
+
+    public function xadd(mixed $key, string $id, array $values, int $maxlen = 0, bool $approx = false, bool $nomkstream = false): Cluster|string|false
+    {
+        return $this->initializeLazyObject()->xadd(...\func_get_args());
+    }
+
+    public function smembers(mixed $key): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->smembers(...\func_get_args());
+    }
+
+    public function sismember(mixed $key, mixed $member): Cluster|bool
+    {
+        return $this->initializeLazyObject()->sismember(...\func_get_args());
+    }
+
+    public function smismember(mixed $key, mixed ...$members): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->smismember(...\func_get_args());
+    }
+
+    public function srem(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->srem(...\func_get_args());
+    }
+
+    public function sadd(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->sadd(...\func_get_args());
+    }
+
+    public function sort(mixed $key, array $options = []): Cluster|array|false|int
+    {
+        return $this->initializeLazyObject()->sort(...\func_get_args());
+    }
+
+    public function sort_ro(mixed $key, array $options = []): Cluster|array|false|int
+    {
+        return $this->initializeLazyObject()->sort_ro(...\func_get_args());
+    }
+
+    public function smove(mixed $srckey, mixed $dstkey, mixed $member): Cluster|bool
+    {
+        return $this->initializeLazyObject()->smove(...\func_get_args());
+    }
+
+    public function spop(mixed $key, int $count = 1): mixed
+    {
+        return $this->initializeLazyObject()->spop(...\func_get_args());
+    }
+
+    public function srandmember(mixed $key, int $count = 1): mixed
+    {
+        return $this->initializeLazyObject()->srandmember(...\func_get_args());
+    }
+
+    public function scard(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->scard(...\func_get_args());
+    }
+
+    public function script(array|string $key_or_address, string $operation, string ...$args): mixed
+    {
+        return $this->initializeLazyObject()->script(...\func_get_args());
+    }
+
+    public function strlen(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->strlen(...\func_get_args());
+    }
+
+    public function hlen(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->hlen(...\func_get_args());
+    }
+
+    public function llen(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->llen(...\func_get_args());
+    }
+
+    public function xack(mixed $key, string $group, array $ids): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->xack(...\func_get_args());
+    }
+
+    public function xclaim(mixed $key, string $group, string $consumer, int $min_idle, array $ids, array $options): Cluster|array|bool
+    {
+        return $this->initializeLazyObject()->xclaim(...\func_get_args());
+    }
+
+    public function xautoclaim(mixed $key, string $group, string $consumer, int $min_idle, string $start, int $count = -1, bool $justid = false): Cluster|array|bool
+    {
+        return $this->initializeLazyObject()->xautoclaim(...\func_get_args());
+    }
+
+    public function xlen(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->xlen(...\func_get_args());
+    }
+
+    public function xgroup(string $operation, mixed $key = null, ?string $group = null, ?string $id_or_consumer = null, bool $mkstream = false, int $entries_read = -2): mixed
+    {
+        return $this->initializeLazyObject()->xgroup(...\func_get_args());
+    }
+
+    public function xdel(mixed $key, array $ids): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->xdel(...\func_get_args());
+    }
+
+    public function xinfo(string $operation, string|null $arg1 = null, string|null $arg2 = null, int $count = -1): mixed
+    {
+        return $this->initializeLazyObject()->xinfo(...\func_get_args());
+    }
+
+    public function xpending(mixed $key, string $group, string|null $start = null, string|null $end = null, int $count = -1, string|null $consumer = null, int $idle = 0): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->xpending(...\func_get_args());
+    }
+
+    public function xrange(mixed $key, string $start, string $end, int $count = -1): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->xrange(...\func_get_args());
+    }
+
+    public function xread(array $streams, int $count = -1, int $block = -1): Cluster|array|bool|null
+    {
+        return $this->initializeLazyObject()->xread(...\func_get_args());
+    }
+
+    public function xreadgroup(mixed $key, string $consumer, array $streams, int $count = 1, int $block = 1): Cluster|array|bool|null
+    {
+        return $this->initializeLazyObject()->xreadgroup(...\func_get_args());
+    }
+
+    public function xrevrange(mixed $key, string $end, string $start, int $count = -1): Cluster|array|bool
+    {
+        return $this->initializeLazyObject()->xrevrange(...\func_get_args());
+    }
+
+    public function xtrim(mixed $key, string $threshold, bool $approx = false, bool $minid = false, int $limit = -1): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->xtrim(...\func_get_args());
+    }
+
+    public function zadd(mixed $key, mixed ...$args): mixed
+    {
+        return $this->initializeLazyObject()->zadd(...\func_get_args());
+    }
+
+    public function zrandmember(mixed $key, array|null $options = null): mixed
+    {
+        return $this->initializeLazyObject()->zrandmember(...\func_get_args());
+    }
+
+    public function zrange(mixed $key, string $start, string $end, mixed $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zrange(...\func_get_args());
+    }
+
+    public function zrevrange(mixed $key, int $start, int $end, mixed $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zrevrange(...\func_get_args());
+    }
+
+    public function zrangebyscore(mixed $key, mixed $start, mixed $end, mixed $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zrangebyscore(...\func_get_args());
+    }
+
+    public function zrevrangebyscore(mixed $key, mixed $start, mixed $end, mixed $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zrevrangebyscore(...\func_get_args());
+    }
+
+    public function zrevrank(mixed $key, mixed $rank, bool $withscore = false): Cluster|array|int|false
+    {
+        return $this->initializeLazyObject()->zrevrank(...\func_get_args());
+    }
+
+    public function zrangestore(mixed $dstkey, mixed $srckey, mixed $start, mixed $end, mixed $options = null): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zrangestore(...\func_get_args());
+    }
+
+    public function zrank(mixed $key, mixed $rank, bool $withscore = false): Cluster|array|int|false
+    {
+        return $this->initializeLazyObject()->zrank(...\func_get_args());
+    }
+
+    public function zrangebylex(mixed $key, mixed $min, mixed $max, int $offset = -1, int $count = -1): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zrangebylex(...\func_get_args());
+    }
+
+    public function zrevrangebylex(mixed $key, mixed $max, mixed $min, int $offset = -1, int $count = -1): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zrevrangebylex(...\func_get_args());
+    }
+
+    public function zrem(mixed $key, mixed ...$args): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zrem(...\func_get_args());
+    }
+
+    public function zremrangebylex(mixed $key, mixed $min, mixed $max): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zremrangebylex(...\func_get_args());
+    }
+
+    public function zremrangebyrank(mixed $key, int $start, int $end): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zremrangebyrank(...\func_get_args());
+    }
+
+    public function zremrangebyscore(mixed $key, mixed $min, mixed $max): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zremrangebyscore(...\func_get_args());
+    }
+
+    public function zcard(mixed $key): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zcard(...\func_get_args());
+    }
+
+    public function zcount(mixed $key, mixed $min, mixed $max): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zcount(...\func_get_args());
+    }
+
+    public function zdiff(array $keys, array|null $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zdiff(...\func_get_args());
+    }
+
+    public function zdiffstore(mixed $dstkey, array $keys): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zdiffstore(...\func_get_args());
+    }
+
+    public function zincrby(mixed $key, float $score, mixed $member): Cluster|false|float
+    {
+        return $this->initializeLazyObject()->zincrby(...\func_get_args());
+    }
+
+    public function zlexcount(mixed $key, mixed $min, mixed $max): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zlexcount(...\func_get_args());
+    }
+
+    public function zmscore(mixed $key, mixed ...$members): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zmscore(...\func_get_args());
+    }
+
+    public function zinter(array $keys, array|null $weights = null, mixed $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zinter(...\func_get_args());
+    }
+
+    public function zintercard(array $keys, int $limit = -1): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zintercard(...\func_get_args());
+    }
+
+    public function zinterstore(mixed $dstkey, array $keys, array|null $weights = null, mixed $options = null): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zinterstore(...\func_get_args());
+    }
+
+    public function zunion(array $keys, array|null $weights = null, mixed $options = null): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zunion(...\func_get_args());
+    }
+
+    public function zunionstore(mixed $dstkey, array $keys, array|null $weights = null, mixed $options = null): Cluster|false|int
+    {
+        return $this->initializeLazyObject()->zunionstore(...\func_get_args());
+    }
+
+    public function zpopmin(mixed $key, int $count = 1): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zpopmin(...\func_get_args());
+    }
+
+    public function zpopmax(mixed $key, int $count = 1): Cluster|array|false
+    {
+        return $this->initializeLazyObject()->zpopmax(...\func_get_args());
+    }
+
+    public function _getKeys(): array|false
+    {
+        return $this->initializeLazyObject()->_getKeys(...\func_get_args());
+    }
+
+    public function _masters(): array
+    {
+        return $this->initializeLazyObject()->_masters(...\func_get_args());
+    }
+
+    public function copy(mixed $srckey, mixed $dstkey, array|null $options = null): Cluster|bool
+    {
+        return $this->initializeLazyObject()->copy(...\func_get_args());
+    }
+}
diff --git a/composer.json b/composer.json
index bdb461be..c89d6672 100644
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,7 @@
         "php": ">=8.2",
         "psr/cache": "^2.0|^3.0",
         "psr/log": "^1.1|^2|^3",
-        "symfony/cache-contracts": "^2.5|^3",
+        "symfony/cache-contracts": "^3.6",
         "symfony/deprecation-contracts": "^2.5|^3.0",
         "symfony/service-contracts": "^2.5|^3",
         "symfony/var-exporter": "^6.4|^7.0"