Redis Object Cache Isolation for Multi-Tenant WordPress Servers

When several WordPress sites share a single Redis instance on one server, their object caches can collide. Two sites can read and write the same cache keys, which leads to stale data, cross-site data bleed, and cache flushes that wipe every site at once.
This is a configuration problem, not a Redis bug. It is solved by giving each site its own key namespace. This guide explains why the collision happens, how to isolate each site with a unique cache key salt, and how to audit a fleet of sites for the problem.
How WordPress object caching uses Redis
WordPress has a built-in object cache API. By default that cache is non-persistent and lives only for the duration of a single request.
A persistent object cache plugin, such as Redis Object Cache, replaces that in-memory cache with a drop-in file at wp-content/object-cache.php. Every wp_cache_get() and wp_cache_set() call then reads and writes to Redis instead of PHP memory.
Each cached value is stored under a key. WordPress builds that key from a cache group and an identifier, for example posts:last_changed or options:alloptions. The drop-in prepends a prefix to that key before sending it to Redis.
The prefix is where isolation is won or lost.
Why sites collide on a shared instance
A single Redis instance has one shared keyspace per logical database. If two sites both write to a key called options:alloptions with no distinguishing prefix, the second write overwrites the first.
On a managed VPS where one Redis server backs many sites, for example a Ploi or RunCloud box hosting several client sites, this is the default state unless each site sets its own prefix.
The symptoms are easy to misread as random plugin bugs:
- A logged-in session or cached option from one site appears on another.
- Cache values look stale even after a purge, because a neighbouring site keeps rewriting the shared key.
- Flushing the cache on one site empties the cache for every site on that Redis instance.
The third symptom is the most damaging. A FLUSHDB triggered by one site clears the whole logical database, so every other site loses its warm cache and takes a performance hit until it rebuilds.
The fix: a unique cache key salt per site
WordPress object cache drop-ins recognise a salt constant that is prepended to every cache key. Setting a unique value per site gives each site its own namespace inside the shared Redis instance.
The core-recognised constant is WP_CACHE_KEY_SALT. The Redis Object Cache plugin also reads its own WP_REDIS_PREFIX constant. Set whichever your drop-in honours. Setting both is safe.
Add the constant to wp-config.php:
// wp-config.php
define( 'WP_CACHE_KEY_SALT', 'client-site-name:' );
define( 'WP_REDIS_PREFIX', 'client-site-name:' );Any unique string works. The problem with hand-picking a string per site is that it does not scale and it drifts. On a server with twenty sites, someone eventually copies a wp-config.php and forgets to change the value, and the collision returns.
Deriving the salt from DB_NAME
A more reliable approach is to derive the salt from something that is already unique per site and already present in wp-config.php: the database name.
Place the constant after the DB_NAME definition:
// wp-config.php, after DB_NAME is defined
define( 'DB_NAME', 'client_site_prod' );
// ...
define( 'WP_CACHE_KEY_SALT', DB_NAME . ':' );
define( 'WP_REDIS_PREFIX', DB_NAME . ':' );Now the salt is deterministic and unique for every site whose database differs, with no per-site editing to forget. When you clone a site and change its database, the cache namespace changes with it automatically.
Edge case: sites that share a database
Some setups run more than one WordPress install against a single database, separated only by a table prefix. There DB_NAME alone is not unique. Fold the table prefix into the salt, since $table_prefix is also defined in wp-config.php:
define( 'WP_CACHE_KEY_SALT', DB_NAME . '_' . $table_prefix );This keeps each install isolated even when the database is shared.
An alternative: separate Redis logical databases
Redis exposes sixteen logical databases by default, indexed 0 to 15. Assigning each site a different index is another way to isolate them:
define( 'WP_REDIS_DATABASE', 3 ); // 0 to 15This works, but it has a hard ceiling of sixteen sites per Redis instance and it does not protect against a mistake where two sites are given the same index. A per-site salt has no such ceiling and is self-documenting in the keyspace. The two techniques can be combined, but the salt is the one that scales.
Scope cache flushes to a single site
Isolating the keyspace also lets you flush one site without touching its neighbours. By default a flush can call FLUSHDB, which clears the entire logical database. Enable selective flushing so the drop-in only deletes keys that match this site’s prefix:
define( 'WP_REDIS_SELECTIVE_FLUSH', true );With a unique prefix and selective flush enabled, a purge on one site removes only that site’s keys and leaves every other site’s cache warm.
Verifying isolation
Inspect the live keyspace with redis-cli. Use --scan rather than KEYS, since KEYS blocks the server on a busy instance.
List the distinct prefixes currently in Redis and how many keys each holds:
redis-cli --scan --pattern '*' | sed 's/:.*//' | sort | uniq -c | sort -rn | headA correctly isolated server shows one prefix per site. Un-prefixed keys, or keys sharing a prefix across sites, point to a site that still needs the constant.
Check a single site’s keys by its database name:
redis-cli --scan --pattern 'client_site_prod:*' | headIf the pattern returns nothing while the site is clearly using Redis, the salt is not being applied and the drop-in or constant needs review.
Auditing a whole fleet
On a server with many sites, find the ones that run a persistent object cache but have no salt or prefix set. The script below reports any wp-config.php that pairs an object-cache.php drop-in with a missing isolation constant.
#!/usr/bin/env bash
# Audit WordPress sites on this server for Redis object cache isolation.
# Flags any site that has a persistent object cache drop-in but no
# unique cache key salt or Redis prefix defined.
WEBROOT="/home" # adjust to your sites root
find "$WEBROOT" -maxdepth 4 -name wp-config.php 2>/dev/null | while read -r cfg; do
site_dir="$(dirname "$cfg")"
# Only care about sites actually using a persistent object cache.
[ -f "$site_dir/wp-content/object-cache.php" ] || continue
if grep -qE "WP_CACHE_KEY_SALT|WP_REDIS_PREFIX" "$cfg"; then
echo "OK: $cfg"
else
echo "NEEDS ISOLATION: $cfg"
fi
doneRun it before and after a remediation pass to confirm every site with a drop-in now defines a unique namespace.
Summary
On a shared or multi-tenant server, a single Redis instance is one shared keyspace. Without a per-site namespace, WordPress sites collide on cache keys, serve stale or cross-site data, and wipe each other’s caches on flush.
The fix is small and durable:
- Set
WP_CACHE_KEY_SALT(andWP_REDIS_PREFIX) inwp-config.php, derived fromDB_NAMEso it stays unique with no manual upkeep. - Add the table prefix to the salt when several installs share one database.
- Enable
WP_REDIS_SELECTIVE_FLUSHso a purge scopes to one site. - Verify with
redis-cli --scanand audit the fleet with a short shell script.
Each site then keeps its own isolated namespace inside the shared instance, which removes the collisions without adding a separate Redis server per site.
References
- Redis Object Cache plugin documentation (WordPress.org plugin directory), for the drop-in behaviour and the
WP_REDIS_PREFIXandWP_REDIS_SELECTIVE_FLUSHconstants. - WordPress Object Cache API in the WordPress Developer Handbook, for
wp_cache_*functions and cache groups. - Redis documentation on logical databases and the
SCANcommand.
Written by the WP Relieve team, which manages WordPress infrastructure across shared and multi-tenant server fleets.