Hosting: gotcha with multiple WP installs, one Redis

If you're using one Redis instance to keep multiple WordPress sites' data, you need to make sure different sites don't bleed over to others. Read how and why.

Hosting: gotcha with multiple WP installs, one Redis

Redis here can be any other key-value store used for object caching; Memcached, for example.

Okay, so the setup is that you have at least two sites with their default table prefixes (both use wp_), and one Redis. In my local development environment, I use Laravel Valet, and have Redis installed via homebrew, and configured so everything Just Works™. You can read about how I set this up here.

The problem is that unless you do additional work, the two sites are going to store their data in the same bucket in the same Redis instance, which is obviously bad. Here’s an example:

$ pwd
/Users/javorszky/Sites/dev2

$ wp option get home
https://dev2.test

$ cd ../dev

$ pwd
/Users/javorszky/Sites/dev

$ wp option get home
https://dev2.test

You should get different values for home from the two different directories, because wp will use the install with the wp-config.php next to it.

If I open the two sites, here’s what I see:

Site on dev2.test displays correct info.
Site on dev.test does not display correct info.

Interestingly the post content seems to be correct.

The reason for this is that WordPress autoloads options (that are to be autoloaded, i.e. most of the core important ones) during its startup with a call to wp_load_alloptions() function that looks like this:

function wp_load_alloptions() {
	global $wpdb;

	if ( ! wp_installing() || ! is_multisite() ) {
		$alloptions = wp_cache_get( 'alloptions', 'options' );
        ...
}

That wp_cache_get is what causes these options from site 1 to bleed over to site 2.

wp_cache_get is defined in our Redis object cache implementation, which is this:

function wp_cache_get($key, $group = '', $force = false, &$found = null)
{
    global $wp_object_cache;

    return $wp_object_cache->get($key, $group, $force, $found);
}

The global $wp_object_cache is set in the same file. The class is declared, and initialised like so:

// # wp-content/plugins/redis-cache/includes/object-cache.php

function wp_cache_init()
{
    global $wp_object_cache;

    if (! ($wp_object_cache instanceof WP_Object_Cache)) {
        $fail_gracefully = ! defined('WP_REDIS_GRACEFUL') || WP_REDIS_GRACEFUL;

        $wp_object_cache = new WP_Object_Cache($fail_gracefully);
    }
}

class WP_Object_Cache
{
    ...
}

Here’s the get method on the class:

public function get($key, $group = 'default', $force = false, &$found = null)
{
    $derived_key = $this->build_key($key, $group);

    if (isset($this->cache[$derived_key]) && ! $force) {
        $found = true;
        $this->cache_hits++;

        return is_object($this->cache[$derived_key]) ? clone $this->cache[$derived_key] : $this->cache[$derived_key];
        ...
}

Essentially it builds an internal redis-specific key from whatever option we want, and then grabs that from the object cache. Going further, build_key is this:

public function build_key($key, $group = 'default')
{
    if (empty($group)) {
        $group = 'default';
    }

    $salt = defined('WP_CACHE_KEY_SALT') ? trim(WP_CACHE_KEY_SALT) : '';
    $prefix = in_array($group, $this->global_groups) ? $this->global_prefix : $this->blog_prefix;

    return "{$salt}{$prefix}:{$group}:{$key}";
}

The final key is made up of the following bits when loading the alloptions cache:

  • group is options, as that’s the second argument to wp_cache_get
  • salt is emptystring, because we do not have WP_CACHE_KEY_SALT set
  • $prefix is wp_ in both cases
  • $key is alloptions in both cases.

Which means that no matter which site you’re querying alloptions, the interal redis key ends up being wp_:options:alloptions. Whichever site re-set that cache after it’s been cleared, wins!

Solution

Add this to your wp-config.php file:

define( 'WP_CACHE_KEY_SALT', 'GiB16F?*EJdJBRT*2-L(|Iya)m77|5g[Nre>ADUR1`K<y7EM(I!Q8UPobSO/j,>>' );

Change the salt to your specific 32 character random string.

Addendum

It would be nice if the salt generator on wordpress.org gave us one of these too without having to reload and use another one as the cache key salt.

Until they do, you can use the one I wrote. That one gives you a salt for the cache salt too :).

Photo by Chris Sabor on Unsplash