In September I wrote on LinkedIn about a production shop on Adobe Commerce 2.4.9 with 413 MB of application cache on disk, while the Redis it had been paying for held zero cache keys. No warning, no log line, every cache type reported as enabled.
Adobe support has since sent a hotfix for it, ACP2E-5507. We applied it this week. This post explains what was wrong, what the fix changes, how we proved it, and you can download the patch at the bottom.
What was wrong
2.4.9 moved the cache layer to Symfony cache adapters. The class that builds the Redis connection, SymfonyAdapterProvider, always builds a TCP address:
redis://<host>:<port>/<database>With a unix socket in env.php the host is a file path, so the result is something like redis:///var/run/redis/cache.sock:0/0. That is not a valid address. The connection fails, and createAdapter() catches the exception and falls back to a filesystem adapter. The exception is not logged.
Sessions use a different handler that does understand sockets, so Redis looks busy if you glance at it. That is why it is so easy to miss.
What the fix changes
The patch touches one file in magento/framework and does three things:
A small helper recognises a socket: a server value that starts with
/orunix://.For phpredis it builds
redis://[password@]/path/to/socket/<database>, a form Symfony accepts. Persistent connections get an id per database and per process, so two cache databases on one socket do not share a connection.For Predis it passes
scheme => 'unix'with the socket path instead of a TCP host and port.
TCP setups take exactly the same path as before. Ryan Hoerr opened a community PR for the same bug in September. The Adobe patch takes the same approach.
How we proved it
Our local environment talks to Redis over TCP, so we put a small proxy in front of it that listens on a unix socket. Then we asked Magento which adapter it actually builds, with the socket as a bare path and as unix://:
Without the patch: a filesystem adapter in both cases, and zero keys in Redis.
With the patch: a Redis adapter in both cases, and the cache keys arrive in the right database.
We also checked that the file on test, acceptance and production is byte for byte the 2.4.9 original, so the patch applies the same everywhere.
Rolling it out
Apply the patch first, deploy, and only then point the cache back at the socket. In that order there is never a moment where the cache silently ends up on disk. Before you switch, run a check that prints which adapter Magento builds, and keep a copy of env.php. The cache section is not part of the config hash, so no app:config:import is needed.
Until you have the patch, the workaround from September still holds: point the cache at 127.0.0.1:6379 instead of the socket.
Check yours
redis-cli -s /path/to/cache.sock info keyspace
du -sh var/cache var/page_cacheAn empty cache database next to a growing var/cache means you have it.
The patch
Download: magento-2.4.9-redis-unix-socket-ACP2E-5507.patch. The paths are relative to the magento/framework package, so it drops straight into cweagans/composer-patches:
"extra": {
"patches": {
"magento/framework": [
"patches/magento-2.4.9-redis-unix-socket-ACP2E-5507.patch"
]
}
}It is written for 2.4.9. Check whether a later 2.4.9 patch release already contains it before you add it. The full diff:
diff --git a/Cache/Frontend/Adapter/SymfonyAdapterProvider.php b/Cache/Frontend/Adapter/SymfonyAdapterProvider.php
index ae9233ab01d..d07d0737752 100644
--- a/Cache/Frontend/Adapter/SymfonyAdapterProvider.php
+++ b/Cache/Frontend/Adapter/SymfonyAdapterProvider.php
@@ -350,12 +350,18 @@ class SymfonyAdapterProvider implements ResetAfterRequestInterface
?int $retryInterval,
?int $connectRetries
) {
+ $socketPath = $this->resolveUnixSocketPath($host);
+
// Build optimized DSN with all connection parameters
$dsnParams = [];
// Add persistent connection parameters
if ($persistent) {
$dsnParams[] = 'persistent=1';
+ if ($socketPath !== null) {
+ // Keep persistent socket connections isolated by database and process.
+ $persistentId = ($persistentId ?? 'default') . ':' . $database . ':' . getmypid();
+ }
if ($persistentId) {
$dsnParams[] = 'persistent_id=' . urlencode($persistentId);
}
@@ -380,9 +386,15 @@ class SymfonyAdapterProvider implements ResetAfterRequestInterface
}
// Build base DSN
- $baseDsn = $password
- ? sprintf('redis://%s@%s:%d/%d', urlencode($password), $host, $port, $database)
- : sprintf('redis://%s:%d/%d', $host, $port, $database);
+ if ($socketPath !== null) {
+ $address = sprintf('%s/%d', $socketPath, $database);
+ $encodedPassword = rawurlencode($password ?? '');
+ } else {
+ $address = sprintf('%s:%d/%d', $host, $port, $database);
+ $encodedPassword = urlencode($password ?? '');
+ }
+ $auth = $password ? $encodedPassword . '@' : '';
+ $baseDsn = 'redis://' . $auth . $address;
// Append DSN parameters
$dsn = $dsnParams ? $baseDsn . '?' . implode('&', $dsnParams) : $baseDsn;
@@ -391,6 +403,18 @@ class SymfonyAdapterProvider implements ResetAfterRequestInterface
return RedisAdapter::createConnection($dsn);
}
+ /**
+ * Resolve a Unix socket path from a backend server value.
+ *
+ * @param string $host
+ * @return string|null
+ */
+ private function resolveUnixSocketPath(string $host): ?string
+ {
+ $path = str_starts_with($host, 'unix://') ? substr($host, strlen('unix://')) : $host;
+ return str_starts_with($path, '/') ? $path : null;
+ }
+
/**
* Create ultra-optimized Predis connection (Symfony-compatible, maximum performance)
*
@@ -413,12 +437,19 @@ class SymfonyAdapterProvider implements ResetAfterRequestInterface
?float $timeout,
?float $readTimeout
) {
- $params = [
- 'scheme' => 'tcp',
- 'host' => $host,
- 'port' => $port,
- 'database' => $database,
- ];
+ $socketPath = $this->resolveUnixSocketPath($host);
+ $params = $socketPath !== null
+ ? [
+ 'scheme' => 'unix',
+ 'path' => $socketPath,
+ 'database' => $database,
+ ]
+ : [
+ 'scheme' => 'tcp',
+ 'host' => $host,
+ 'port' => $port,
+ 'database' => $database,
+ ];
if ($password) {
$params['password'] = $password;