July 7, 2026 · Guides

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:

  1. A logged-in session or cached option from one site appears on another.
  2. Cache values look stale even after a purge, because a neighbouring site keeps rewriting the shared key.
  3. 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 15

This 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 | head

A 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:*' | head

If 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
done

Run 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:

  1. Set WP_CACHE_KEY_SALT (and WP_REDIS_PREFIX) in wp-config.php, derived from DB_NAME so it stays unique with no manual upkeep.
  2. Add the table prefix to the salt when several installs share one database.
  3. Enable WP_REDIS_SELECTIVE_FLUSH so a purge scopes to one site.
  4. Verify with redis-cli --scan and 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_PREFIX and WP_REDIS_SELECTIVE_FLUSH constants.
  • WordPress Object Cache API in the WordPress Developer Handbook, for wp_cache_* functions and cache groups.
  • Redis documentation on logical databases and the SCAN command.

Written by the WP Relieve team, which manages WordPress infrastructure across shared and multi-tenant server fleets.