Managing WordPress Object Cache with Memcached: Per-Site Flush, Monitoring & Optimization
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
- ✅ Flushed 318 keys using
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
-
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 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:
-
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
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:
-
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!