Managing WordPress Object Cache with Memcached: Per-Site Flush, Monitoring & Optimization

Mainul HasanMainul Hasan
11 min read

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

FeatureMemcachedRedis
Data structureKey-value onlySupports advanced types (lists, sets, etc.)
PersistenceIn-memory only (no persistence)Optional persistence to disk
Use caseLightweight, fast for object cachingMore flexible, often used in Laravel or apps needing queues
WordPress fitGreat 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, "stats\n");
      $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) . " MB\n";
      $output .= "Memory Limit       : " . round($stats['limit_maxbytes'] / 1024 / 1024, 2) . " MB\n";
      $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

  • Memory Used vs Limit: If usage is consistently close to the limit (e.g., 60 MB of 64 MB), eviction is likely.

  • Hit/Miss Ratio: A high hit rate (90%+) means the cache is effective.

  • Evictions: This shows how many old entries Memcached had to delete to make space. Frequent evictions = not enough memory.

  • Items Stored: Number of keys in cache.

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 items\r\n");
      $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 200\r\n");
          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 keys\n", $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:

  • Why a specific site is bloating memory

  • Whether your salt is missing (0 keys = wrong or undefined salt)

  • Sites not benefiting from caching at all

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:

  • Multiple high-traffic sites on the same Memcached instance

  • Large WooCommerce or multilingual setups

  • Default memory limit too small (e.g., 64 MB)

๐Ÿ’ก 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

  • WooCommerce stores: Dynamic product and cart pages, session data, etc.

  • Multilingual websites: Lots of options and translation strings cached

  • Membership or login-based sites: Auth checks and custom queries

  • Content-heavy blogs with logged-in users (e.g., WebDevStory)

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

โŒ When Object Cache May Not Be Worth It

  • Small static brochure sites

  • Low-traffic blogs with infrequent updates

  • Minimal plugin usage

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

  • Static and updated rarely

  • Lightweight in theme and plugins

  • Visited occasionally, mostly by anonymous users

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

๐Ÿ’ก Rule of Thumb

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

  • All keys go into a global pool

  • You lose the ability to flush per site

  • Monitoring tools canโ€™t differentiate usage

Sample setup per wp-config.php:

define('WP_CACHE_KEY_SALT', 'webdevstory_');

๐Ÿšซ Avoid Full Flush Unless Using Dedicated Memcached

  • Unless youโ€™re on a single-site server, never flush the entire cache without confirming

  • Use a plugin (like ours) that prevents accidental full flush without salt

  • Full flushes can cause downtime or performance drops across other sites on the same instance

๐Ÿ“Š Use Monitoring to Balance Memory

Whether through:

  • A custom PHP stats script

  • LiteSpeed cache panel

  • Query Monitor plugin (advanced)

Regular monitoring helps answer:

  • Should you increase memory?

  • Is one site bloating the cache?

  • Are your hit rates improving?

๐Ÿง  Be Aware of Key Growth and Expiry

Memcached stores keys in memory until:

  • They expire (default: 0 = never)

  • Theyโ€™re evicted due to space pressure

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

  • Waste memory on stale data

  • Trigger unnecessary evictions

  • Hurt performance rather than improve it

๐Ÿ“Œ 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:

  • Using Memcached wisely in shared or VPS setups

  • Defining a proper WP_CACHE_KEY_SALT for isolation

  • Monitoring usage and tuning memory limits

  • Having tools to flush intelligently, not blindly

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:

  • โœ… Admin button to flush object cache

  • โœ… Selective flush by salt (WP_CACHE_KEY_SALT)

  • โœ… Full-flush confirmation if no salt is defined

  • โœ… Memcached backend detection

  • โœ… Safe UI feedback with hit/miss logging options

<?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 class="notice notice-success is-dismissible"><p>' .
              esc_html__('โœ… Flushed ' . $deleted . ' object cache keys using WP_CACHE_KEY_SALT.', 'simple-object-cache-flusher') .
              '</p></div>';
          } else {
              echo '<div class="notice notice-warning is-dismissible"><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 class="wrap">
          <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" class="button button-primary"
                  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:

  • ๐Ÿ‘ Found this guide helpful? Give it a like!

  • ๐Ÿ’ฌ 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!

Join the Community โ†’

0
Subscribe to my newsletter

Read articles from Mainul Hasan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mainul Hasan
Mainul Hasan

Hello, I'm Mainul Hasan. Currently, Iโ€™m working as a Teaching Assistant at the University of Oslo and pursuing my Masterโ€™s program in Informatics: Programming and System Architecture. Before this, I worked as a Web Developer in several companies, gaining experience in different tech stacks and programming languages such as JavaScript, PHP, and Python. Inspired by my professional journey and desire for lifelong learning, I've started to write about Tech & Lifestyle, Web Development, and the Digital Nomad Life. Here, I share my knowledge and experiences, engage in discussions with my readers, and strive to make my spare time more meaningful. Feel free to connect with me on LinkedIn or email me at hi@mmainulhasan.com for any potential opportunities. Happy Learning!