Short answer
For WordPress, Redis and Memcached are both good persistent object cache backends, and the choice is mostly operational rather than about speed. WordPress only needs key-value operations, which both handle well. Choose on what your platform runs, how mature the drop-in is, how group flushing works and what happens when the cache restarts. Then spend your effort on how your code uses the cache, because that is where most real gains and outages come from.
On WordPress VIP the decision is made for you: each environment gets its own Memcached cluster. Everywhere else you choose, and most comparisons stop at “Redis has more features”. This one covers the differences that actually change how a busy WordPress site behaves: memory and eviction, threading, flushing, serialisation, and the stampede problem that neither backend solves for you.
What the persistent object cache actually does
WordPress always has an object cache. By default it is non-persistent: it lives in PHP memory for one request and is thrown away at the end. A persistent backend arrives as an object-cache.php drop-in in wp-content, which replaces the default implementation so that wp_cache_get() and wp_cache_set() survive between requests.
Two consequences matter more than the backend you pick:
- Transients move out of the database. With a persistent object cache,
set_transient()writes to the cache instead ofwp_options. Code that queries the options table for transients directly will stop finding them. VIP’s documentation calls this out explicitly. - Option, post, term and meta lookups that core already caches stop hitting MySQL on every request. Most of the benefit comes from these core caches, not from your custom cache calls.
Redis vs Memcached: the differences that matter for WordPress
| Memcached | Redis (and API-compatible forks such as Valkey) | |
|---|---|---|
| Data model | Plain key-value | Key-value plus hashes, sets, sorted sets, lists, streams |
| Threading | Multithreaded: worker threads share the cache | Commands run on a single main thread. Recent versions can offload network I/O to threads |
| Memory management | Slab allocator: fixed-size chunks per slab class, LRU per class | General allocator; configurable eviction policy (maxmemory-policy) |
| Default max item size | 1 MB (configurable) | Strings up to 512 MB, which is far beyond anything sensible to cache |
| Survives a restart | No. Restart means a cold cache | Optional, via RDB snapshots or AOF |
| Replication / failover | None built in; clients spread keys across nodes | Replicas, Sentinel and Cluster |
| Flushing one cache group | Not natively possible; drop-ins have to emulate it | Possible with key scans or Lua; the Redis Object Cache plugin uses a Lua script |
| WordPress drop-in ecosystem | Smaller; mostly host-maintained | Larger: Redis Object Cache, Object Cache Pro and host-provided drop-ins |
Slab allocation vs a data-structure store
Memcached divides memory into pages, cuts each page into fixed-size chunks for a slab class, and stores each item in the smallest chunk that fits. The Memcached internals documentation is clear about the trade-off. Allocation is predictable and fragmentation-resistant, but space is wasted inside chunks, and each slab class has its own LRU. A workload that suddenly changes its item sizes can evict items in one class while another class has room to spare. WordPress caches a mix of tiny option values and large serialised arrays, so watch eviction counts per class rather than total memory.
Redis treats the cache as a general data store. For a WordPress object cache that flexibility is mostly unused, because the object cache API is get, set, add, delete and increments. The features that do matter are the operational ones: persistence, replicas and group flushing.
Single-threaded execution: real, but rarely the bottleneck
The Redis latency documentation states that Redis serves requests sequentially on one thread. A slow command therefore makes every other client wait. GET and SET run in constant time, so WordPress’s normal traffic does not cause this. The commands that do are the ones operators reach for during an incident: KEYS to find cache entries, and pattern-based deletes that walk the whole keyspace. If your drop-in or tooling flushes by pattern, check how it does it before you need it under load.
Memcached’s multithreading is a genuine advantage on a large shared cache node. For most WordPress deployments, network round trips dominate either way. Fewer, batched lookups (wp_cache_get_multiple(), available since WordPress 5.5) help more than switching engines.
Where WordPress VIP fits
The current VIP object cache documentation says each environment is provisioned with its own siloed Memcached cluster. It also says cache entries must stay under 1 MB, transients are stored in the object cache rather than wp_options, and wp_cache_add() and wp_cache_set() default to no expiration, so entries live until LRU eviction. VIP does not document its reasons for choosing Memcached, so treat any explanation you read as inference. A pure, disposable cache that never needs persistence is a natural fit for Memcached’s model.
If you build for VIP, design for those constraints locally: keep values small, never rely on a cache entry surviving, and do not query transients from the database. The wider platform rules are in Engineering for WPVIP.
Flushing: the difference you notice in production
WordPress 6.1 added wp_cache_flush_group(). Whether it works depends on the drop-in, so check with wp_cache_supports( 'flush_group' ) rather than function_exists(), because core polyfills the newer functions. On Memcached, a drop-in can only emulate group flushes, typically by versioning keys. On Redis, the popular Redis Object Cache plugin flushes groups with a Lua script, and falls back to FLUSHDB if you disable that.
The safe habit on either backend: never design a feature that depends on a full cache flush. A full flush on a busy site turns every request into a cache miss at the same moment, which is exactly the stampede described next.
Preventing the cache stampede
When a popular entry expires under heavy traffic, every PHP worker that misses it rebuilds it at once, all running the same expensive query. Neither backend prevents this. Your code has to.
The pattern below keeps the original draft’s core idea: wp_cache_add() only succeeds if the key does not exist, so it works as a lock. It adds a soft expiry so that losers serve a stale copy instead of waiting:
function acme_get_report( int $report_id ) {
$group = 'acme_reports';
$key = "report_{$report_id}";
$lock = "{$key}_lock";
$cached = wp_cache_get( $key, $group );
if ( is_array( $cached ) && $cached['expires'] > time() ) {
return $cached['data']; // Fresh hit.
}
// Only one worker wins; add() fails if the lock key already exists.
if ( ! wp_cache_add( $lock, 1, $group, 30 ) ) {
if ( is_array( $cached ) ) {
return $cached['data']; // Stale but instant while someone else rebuilds.
}
usleep( 100000 ); // Cold cache: give the winner 100 ms, once.
$cached = wp_cache_get( $key, $group );
return is_array( $cached ) ? $cached['data'] : acme_build_report( $report_id );
}
try {
$data = acme_build_report( $report_id );
// Soft expiry inside the value; the entry itself lives longer,
// so a stale copy exists for the next rebuild.
wp_cache_set(
$key,
array( 'data' => $data, 'expires' => time() + HOUR_IN_SECONDS ),
$group,
DAY_IN_SECONDS
);
} finally {
wp_cache_delete( $lock, $group );
}
return $data;
}
Three details make this safe:
- The lock has its own short expiry, so a worker that dies mid-rebuild cannot hold it forever.
- The lock is only meaningful with a persistent backend. Without one,
wp_cache_add()only sees the current request, so every worker “wins”. Test the pattern against the real backend. - The value is wrapped in an array, so a legitimately empty or
falseresult is still distinguishable from a miss. If you cache scalars directly, use the fourth$foundargument ofwp_cache_get().
Serialisation and igbinary
Every value crosses the wire as a serialised string. PHP’s native serialize() is verbose. The binary igbinary serialiser usually produces smaller payloads and less unserialise work. The size of the gain depends on your data, so measure it on your own cache rather than trusting a headline percentage.
- Memcached: the PECL extension’s
memcached.serializersetting defaults to igbinary when the extension was built with it, then msgpack, then PHP. - Redis Object Cache: set
WP_REDIS_IGBINARYtotrue.
Changing the serialiser makes existing entries unreadable, so flush the cache as part of that deploy, during low traffic.
Key prefixes, multisite and shared servers
In multisite, WordPress already separates per-site data: drop-ins prefix non-global groups with the blog ID. Cross-site leaks come from two other places:
- Custom groups registered as global that hold per-site data.
- Several installs sharing one cache server without a unique prefix. Set
WP_CACHE_KEY_SALT, orWP_REDIS_PREFIXfor Redis Object Cache, which replaces the salt. Give staging and production different values.
Large networks have further cache and database concerns, covered in Scaling WordPress Multisite: database sharding.
A decision guide
- On WordPress VIP: Memcached, provided. Design for small values and eviction at any time.
- Your host offers one managed option: use it. A well-run cache you do not operate beats a better engine you do.
- Self-managed, need group flushes, replicas or a warm restart: Redis or Valkey with a maintained drop-in.
- Self-managed, pure cache, very high concurrency on few nodes: Memcached is simple and multithreaded, and nothing is lost by its lack of persistence.
- Either way: batch lookups, cap value size, use stampede protection on expensive keys, and alert on evictions and hit rate.
Long-running WP-CLI jobs need one extra habit, because the runtime cache grows with every object they touch. Flushing it between batches is covered in WP-CLI at Scale.
Questions people actually ask
- Why does WordPress VIP use Memcached instead of Redis?
- VIP’s documentation confirms that each environment gets its own Memcached cluster, but it does not explain the choice. The practical reading is that VIP treats the object cache as purely disposable, so Redis’s persistence and data structures add little. For you, the consequence matters more than the reason: values must stay under 1 MB and any entry can disappear.
- Is Redis faster than Memcached for WordPress?
- For WordPress’s get/set workload, neither has a meaningful advantage that holds across setups. Network round trips, value size and the number of lookups per request matter more. Cut lookups with
wp_cache_get_multiple()and keep values small before you switch engines for speed. - Can object caching break nonces or logged-in pages?
- Not by itself. Nonces break when rendered HTML containing them is cached and served to other users, which is usually a page-cache or fragment-cache mistake. The object-cache version of the bug is storing user-specific data under a shared key. Include the user ID in the key, or do not cache it.
- Do I need an object cache if I already have a page cache?
- Yes, on any site with logged-in users, admin traffic, REST or AJAX requests, or uncached pages. The page cache only serves anonymous page views. Everything else runs PHP and hits the database, and that traffic is where the object cache earns its keep.
Is your object cache helping or hiding problems?
I review WordPress caching set-ups for stampede risk, oversized values, unsafe groups and the right backend for your platform, including VIP.