Safely Flush Memcached Object Cache in WordPress (Per Site with WP_CACHE_KEY_SALT)

by — August 7, 2025
Reading Time: 12 mins read

Table of Contents

We run multiple WordPress sites on a single VPS, all using Memcached for object caching. The problem? Flushing the object cache for one site risks wiping the cache for all others sharing the same memory pool.

In this post, I’ll walk you through:

  • Why object caching is critical for WordPress performance
  • How Memcached behaves in shared VPS environments
  • And how we built a custom plugin to safely flush cache per site using WP_CACHE_KEY_SALT

If you’re managing multiple sites, I hope this guide helps you optimize caching with clarity and confidence.

1. Why Object Caching Matters in WordPress

Object caching temporarily stores the results of complex database queries, WordPress options, or transients in memory so they can be quickly reused. This avoids hitting the MySQL database unnecessarily and significantly reduces load time.

Why it matters:

  • Faster page loads (especially for dynamic or logged-in requests)
  • Reduced database stress under traffic spikes
  • Essential for scaling WordPress on high-traffic sites

Memcached vs Redis for WordPress

Feature Memcached Redis
Data structure Key-value only Supports advanced types (lists, sets, etc.)
Persistence In-memory only (no persistence) Optional persistence to disk
Use case Lightweight, fast for object caching More flexible, often used in Laravel or apps needing queues
WordPress fit Great for object cache (transients, queries) Also great; some plugins prefer Redis

In many cases, Memcached is faster and simpler to configure — and it’s widely supported by LiteSpeed and cPanel providers.

2. How Memcached Works in Shared VPS Environments

Default Port 11211 and Shared Instances

Memcached by default runs on port 11211. Unless explicitly isolated per app, all websites on the server connect to the same instance. That means:

  • A flush command (flush_all) affects all keys from all sites.
  • There’s no native separation of site-specific data.

Why You Need to Namespace Your Keys

WordPress supports namespacing via:


  define('WP_CACHE_KEY_SALT', 'yoursiteprefix_');
  

This is essential. It prepends a unique string to every cache key generated by WordPress, allowing plugins or scripts (like ours) to selectively flush only your site’s cache.

Without it, you can’t safely delete keys without affecting others.

Memory Limits: Default vs Optimized

Default Memcached allocation on many cPanel servers is 64 MB.

You can monitor it using tools like:

  • getStats() via script
  • WordPress Object Cache Pro
  • Custom scripts with fsockopen or Telnet

Example output:


    Memory Used  : 37 MB
    Memory Limit : 64 MB
    Evictions    : 1.4M (means old data is being overwritten)
    Hit Rate     : 94%
  

What Happens When Multiple Sites Share the Same Instance

  • Cache collisions (without WP_CACHE_KEY_SALT)
  • Overwrites and evictions
  • Full flushes affect every site
  • Monitoring gets confusing unless you prefix keys and track them separately

That’s why we decided to build our own flusher plugin tailored to a multi-site VPS scenario.

3. Building a Custom Cache Flusher Plugin

💡 The Problem

WordPress provides a built-in function:

wp_cache_flush();

But there’s a catch:

  • It only works via WP-CLI.
  • If used programmatically, it often gets blocked in object-cache.php or flushes everything — not safe for shared environments.

🔨 Plugin Features

  • Adds a “Flush Object Cache” option under Tools → Flush Object Cache
  • Detects cache backend: Memcached, Redis, APC, or unknown
  • Checks for WP_CACHE_KEY_SALT
  • If defined → flushes only matching keys

🧪 Technical Highlights

  • Uses Memcached::getAllKeys() when available
  • Uses delete() for each key that starts with the defined salt
  • Handles extensions that don’t support key enumeration (e.g., fallback message)
  • Displays real-time status messages like:
    • ✅ Flushed 318 keys using WP_CACHE_KEY_SALT
    • ⚠️ Salt not defined and no confirmation to flush all cache
    • ❌ Backend not detected

4. Monitoring Memcached Usage

Once object caching is active, blindly assuming it’s helping is a mistake. You need visibility into how your Memcached instance is performing, especially if it’s shared among multiple sites.

We built a lightweight PHP script that outputs useful Memcached stats:


  <?php
  /**
   * Memcached Monitor – WordPress-safe PHP script
   * Shows or logs Memcached usage: items, memory, hits, evictions
 */
 header('Content-Type: text/plain');
 function socf_get_memcached_stats() {
  $sock = @fsockopen('127.0.0.1', 11211);
  if (!$sock) {
    return "❌ Could not connect to Memcached at 127.0.0.1:11211.";
  }
  fwrite($sock, "statsn");
  $stats = [];
  while (!feof($sock)) {
    $line = fgets($sock, 128);
    if (strpos($line, 'STAT') === 0) {
      $parts = explode(' ', trim($line));
      $stats[$parts[1]] = $parts[2];
    } elseif (trim($line) === 'END') {
      break;
    }
  }
  fclose($sock);
  // Output
  $output  = "✅ Memcached Status:n";
  $output .= "-------------------n";
  $output .= "Items Stored       : " . number_format($stats['curr_items']) . "n";
  $output .= "Memory Used        : " . round($stats['bytes'] / 1024 / 1024, 2) . " MBn";
  $output .= "Memory Limit       : " . round($stats['limit_maxbytes'] / 1024 / 1024, 2) . " MBn";
  $output .= "Cache Hits         : " . number_format($stats['get_hits']) . "n";
  $output .= "Cache Misses       : " . number_format($stats['get_misses']) . "n";
  $output .= "Evictions          : " . number_format($stats['evictions']) . "n";
  $output .= "Hit Rate           : " . (
  ($stats['get_hits'] + $stats['get_misses']) > 0
  ? round($stats['get_hits'] / ($stats['get_hits'] + $stats['get_misses']) * 100, 2)
  : 0
  ) . "%n";
  return $output;
}
// Show or log depending on context
echo socf_get_memcached_stats();

You can run this script from your browser or cron to monitor performance.


  ✅ Memcached Status:
  ----------------------
  Items Stored  : 107,705
  Memory Used   : 37.36 MB
  Memory Limit  : 64 MB
  Cache Hits    : 631,841,892
  Cache Misses  : 35,675,800
  Evictions     : 1,476,500
  Hit Rate      : 94.66%

What These Metrics Mean:

Tracking Per-Site Usage by Prefix (Salt)

We extended our script to group keys by WP_CACHE_KEY_SALT prefix and output something like:


  <?php
  /**
   * Memcached Site-wise Monitor (with defined salts)
   * Groups keys by exact WP_CACHE_KEY_SALT values.
 */
 header('Content-Type: text/plain');
 // Define your known salts (must match what's in wp-config.php)
 $salt_prefixes = [
 'webdevstory_'     => 'WebDevStory',
 'site2_'       => 'Site 2',
 'site3_' => 'Site 3',
 'site4_'      => 'Site 4',
 ];
 function socf_get_sitewise_memcached_keys($salt_prefixes) {
  $sock = @fsockopen('127.0.0.1', 11211);
  if (!$sock) {
    return "❌ Could not connect to Memcached at 127.0.0.1:11211.";
  }
  fwrite($sock, "stats itemsrn");
  $slabs = [];
  while (!feof($sock)) {
    $line = fgets($sock, 128);
    if (preg_match('/STAT items:(d+):number/', $line, $matches)) {
      $slabs[] = $matches[1];
    } elseif (trim($line) === 'END') {
      break;
    }
  }
  $site_counts = array_fill_keys(array_values($salt_prefixes), 0);
  $site_counts['Untracked'] = 0;
  foreach (array_unique($slabs) as $slab) {
    fwrite($sock, "stats cachedump $slab 200rn");
    while (!feof($sock)) {
      $line = fgets($sock, 512);
      if (preg_match('/ITEM ([^s]+) /', $line, $matches)) {
        $key = $matches[1];
        $matched = false;
        foreach ($salt_prefixes as $salt => $label) {
          if (strpos($key, $salt) === 0) {
            $site_counts[$label]++;
            $matched = true;
            break;
          }
        }
        if (!$matched) {
          $site_counts['Untracked']++;
        }
      } elseif (trim($line) === 'END') {
        break;
      }
    }
  }
  fclose($sock);
  $output  = "📊 Memcached Key Usage by Site (Salt Matching):n";
  $output .= "---------------------------------------------n";
  foreach ($site_counts as $label => $count) {
    $output .= sprintf("🔹 %-20s : %d keysn", $label, $count);
  }
  return $output ?: "No keys found.";
}
echo socf_get_sitewise_memcached_keys($salt_prefixes);

  📊 Memcached Key Usage by Site (Salt Matching):
    -----------------------------------------------
  Site 1       : 0 keys
  Site 2       : 429 keys
  Site 3       : 164 keys
  Site 4       : 1273 keys
  Untracked    : 7 keys

This is invaluable when diagnosing:

When and Why Evictions Happen

Evictions occur when Memcached runs out of memory and starts removing old keys (often the least recently used). Common causes:

💡 We upgraded our instance to 512 MB after observing high eviction counts and were able to reduce them significantly.

5. Deciding Which Sites Should Use Object Cache

Not all websites need object caching — or at least not Memcached/Redis-level caching.

✅ When Object Cache Is Helpful

In these cases, object caching significantly reduces query load and boosts TTFB.

❌ When Object Cache May Not Be Worth It

For example, we disabled Memcached for a site which is:

Using object cache here would just consume space in shared memory and increase complexity.

💡 Rule of Thumb:

Scenario Use Object Cache?
WooCommerce with 500+ products ✅ Yes
Blog with 50 posts, no login ❌ Probably not
Multilingual portfolio site ✅ Yes
Static info page ❌ Skip it
Membership site ✅ Absolutely

6. Best Practices for Multi-site Memcached Use

✅ Always Define WP_CACHE_KEY_SALT

This is critical. Without it:

Sample setup per wp-config.php:

define('WP_CACHE_KEY_SALT', 'webdevstory_');

🚫 Avoid Full Flush Unless Using Dedicated Memcached

📊 Use Monitoring to Balance Memory

Whether through:

Regular monitoring helps answer:

🧠 Be Aware of Key Growth and Expiry

Memcached stores keys in memory until:

If your plugin or theme stores too many transients or uses long TTLs (wp_cache_set( 'key', 'value', 'group', 3600 )), you may:

📌 Set sensible expiration times and review what’s being cached if you suspect bloat.

Final Thoughts

Object caching can supercharge your WordPress site — but only if managed correctly.

From small blogs to large WooCommerce stores, the difference between efficient and wasteful caching comes down to:

We learned the hard way: full cache flushes, shared memory conflicts, and missed key prefixes can cost you performance and stability.

Try Our Simple Object Cache Flusher Plugin

To help manage this, we built a lightweight plugin with:


  <?php
  /**
   * Plugin Name: Simple Object Cache Flusher
   * Description: Adds an admin menu with backend info and a button to flush only this site's object cache (Memcached) using WP_CACHE_KEY_SALT.
   * Version: 1.3
   * Author: Mainul Hasan
   * Author URI: https://www.webdevstory.com/
   * License: GPL2+
   * License URI: https://www.gnu.org/licenses/gpl-2.0.html
   * Text Domain: simple-object-cache-flusher
   * Domain Path: /languages
 */
 add_action('admin_menu', function () {
  add_management_page(
  __('Flush Object Cache', 'simple-object-cache-flusher'),
  __('Flush Object Cache', 'simple-object-cache-flusher'),
  'manage_options',
  'flush-object-cache',
  'socf_admin_page'
  );
});
function socf_admin_page() {
  if (!current_user_can('manage_options')) {
    wp_die(__('Not allowed.', 'simple-object-cache-flusher'));
  }
  $cache_backend = __('Unknown', 'simple-object-cache-flusher');
  if (file_exists(WP_CONTENT_DIR . '/object-cache.php')) {
    $file = file_get_contents(WP_CONTENT_DIR . '/object-cache.php');
    if (stripos($file, 'memcached') !== false) {
      $cache_backend = 'Memcached';
    } elseif (stripos($file, 'redis') !== false) {
      $cache_backend = 'Redis';
    } elseif (stripos($file, 'APC') !== false) {
      $cache_backend = 'APC';
    } else {
      $cache_backend = __('Custom/Other', 'simple-object-cache-flusher');
    }
  } else {
    $cache_backend = __('Not detected / Disabled', 'simple-object-cache-flusher');
  }
  if (isset($_POST['socf_flush'])) {
    check_admin_referer('socf_flush_cache', 'socf_nonce');
    $prefix = defined('WP_CACHE_KEY_SALT') ? WP_CACHE_KEY_SALT : '';
    $deleted = 0;
    $error_msg = '';
    if ($prefix && class_exists('Memcached')) {
      $host = apply_filters('socf_memcached_host', '127.0.0.1');
      $port = apply_filters('socf_memcached_port', 11211);
      $mem = new Memcached();
      $mem->addServer($host, $port);
      if (method_exists($mem, 'getAllKeys')) {
        $all_keys = $mem->getAllKeys();
        if (is_array($all_keys)) {
          foreach ($all_keys as $key) {
            if (strpos($key, $prefix) === 0) {
              if ($mem->delete($key)) {
                $deleted++;
              }
            }
          }
        }
      } else {
        $error_msg = 'Your Memcached extension does not support key enumeration (getAllKeys). Partial flush not possible.';
      }
    }
    if ($deleted > 0) {
      echo '<div><p>' .
      esc_html__('✅ Flushed ' . $deleted . ' object cache keys using WP_CACHE_KEY_SALT.', 'simple-object-cache-flusher') .
      '</p></div>';
    } else {
      echo '<div><p>' .
      esc_html__('⚠️ No matching keys deleted. Either WP_CACHE_KEY_SALT is not set, or key listing is unsupported. ', 'simple-object-cache-flusher') .
      esc_html($error_msg) .
      '</p></div>';
    }
  }
  ?>
  <div>
  <h1><?php esc_html_e('Flush Object Cache', 'simple-object-cache-flusher'); ?></h1>
  <p><strong><?php esc_html_e('Backend detected:', 'simple-object-cache-flusher'); ?></strong> <?php echo esc_html($cache_backend); ?></p>
  <form method="post">
  <?php wp_nonce_field('socf_flush_cache', 'socf_nonce'); ?>
  <p>
  <input type="submit" name="socf_flush"
  value="<?php esc_attr_e('Flush Object Cache Now', 'simple-object-cache-flusher'); ?>"/>
  </p>
  </form>
  <p><?php esc_html_e("This will flush the Memcached object cache if available on your server. Tries to delete only this site's keys if salt is defined.", 'simple-object-cache-flusher'); ?></p>
  <?php if ($cache_backend === __('Not detected / Disabled', 'simple-object-cache-flusher')) : ?>
  <p style="color: red;"><?php esc_html_e('No object cache backend detected. You may not be using object caching.', 'simple-object-cache-flusher'); ?></p>
  <?php endif; ?>
  </div>
  <?php
}
add_action('plugins_loaded', function () {
  load_plugin_textdomain('simple-object-cache-flusher', false, dirname(plugin_basename(__FILE__)) . '/languages');
});

👉 Check it out on GitHub

🚀 Before You Go:

  • 💬 Got thoughts? Share your insights!
  • 📤 Know someone who needs this? Share the post!
  • 🌟 Your support keeps us going!

💻 Level up with the latest tech trends, tutorials, and tips - Straight to your inbox – no fluff, just value!

Leave a Reply

Your email address will not be published. Required fields are marked *

Support WebDevStory
Pictory 20% off
The Book Every Programmer Swears By
Recommended Hosting
Earn Money
Upgrade Your Skills
Work Comfortably Anywhere

Note: Some links on this page might be affiliate links. If you make a purchase through these links, I may earn a small commission at no extra cost to you. Thanks for your support!