Skip to content
System nominalUTC --:--:--Articles 041

Redis or Memcached: What the WordPress Object Cache Needs

Both backends store the same WordPress keys. What decides the choice is eviction, restarts, shared servers and what wp_cache_flush really clears.

Tomas Lindqvist · @tlindqvist

Hardening and operations

Topic
INFRA
Section
Hosting & Infrastructure
Published
07 AUG 2026
Reading time
8 MIN
Words
1907W
Technical cover graphic for Redis or Memcached: What the WordPress Object Cache Needs
Generated schematic — INFRA series, REV 2.6.

Somebody moves a store from Memcached to Redis, refreshes the homepage twice, and calls it fixed. Four days later a product import finishes and wp_cache_flush() runs. Every page on the server goes cold at the same moment, and two other stores share that Redis instance. The migration was fine. The assumption underneath it was wrong.

Redis against Memcached is usually written up as a feature comparison, and the feature comparison is the least useful part of the decision. Both store PHP values under string keys, and both answer a read in well under a millisecond on a local socket. What separates them is what happens when memory fills, what happens when the process restarts, and who else is on the box.

There is a prior question that rarely gets asked before the migration: how much of your traffic touches the object cache at all? On a site behind a full-page cache the honest answer is often "almost none of the anonymous page views".

The object cache holds four kinds of thing with four different lifetimes

An object cache is not a page cache and it is not a query cache. It is a key-value store that WordPress consults in front of several unrelated things, and the entries behave nothing alike.

The options blob. On an uncached request WordPress runs one query for every row in wp_options marked autoload and keeps the whole result set as a single entry under the key alloptions in the options group. That read happens during wp-settings.php, before WordPress knows whether it is serving a page view or a REST call. It is also the largest value most sites place in an object cache, which is what makes the item-size limit below a real limit rather than a theoretical one.

Transients. An expiring transient is written to the options table with a timeout row, and modern versions mirror the value into the object cache so the read never reaches the database. Non-expiring transients are ordinary options with autoload off that live forever unless something deletes them.

Objects that core primes as it reads. Post objects, term objects, user objects and post meta lookups land in the cache in groups named for their type. These are the entries a plugin loop churns hardest, because every write to post meta drops an entry the next read rebuilds from the database. A page rendering fifty products from an unprimed loop can refill the same keys several times.

Values that only mean something inside one request. Core and plugins write entries whose meaning depends on the current user or a nonce, and a persistent backend stores them anyway. That is harmless while the key identifies its context, and a hazard when it does not, because that is how one visitor's value gets served to another for the next hour.

No eviction policy can tell these four apart. To Redis and to Memcached they are all blobs competing for the same memory.

Memcached is memory only, and it evicts before it tells you

Memcached allocates a fixed amount of RAM and never touches disk. When that RAM fills it reclaims space by evicting the least recently used items, and by default it logs nothing about doing so. Your wp_cache_set() call returned true when the value was written. Hours later the value is gone and the site is quietly paying the query cost again. Nothing in the request path reports it.

Two properties fall out of that design. A Memcached restart produces a completely cold site, since there was never anything to persist. And there is a hard ceiling on a single value: the default maximum item size is 1 MB, applied per item rather than to the pool. A serialised array of product data or a plugin settings object can cross it. When it does, the store fails and WordPress carries on without a cache entry.

Item size limit

The 1 MB figure is a server flag, not a WordPress setting, and raising it needs a change on every server and client in the pool. If you cannot raise it, stop writing a megabyte into one key, because the object cache API cannot split a value across entries.

Redis makes you choose an eviction policy, and the default guess is wrong

Redis evicts too, but it makes you decide when and how. maxmemory sets the ceiling, maxmemory-policy sets the behaviour at that ceiling, and the policy you pick changes what a full cache does to the site.

For a dedicated object cache the sane choice is allkeys-lru or, on Redis 4 and newer, allkeys-lfu, which evicts by access frequency and holds on to the entries a busy store reads constantly. The volatile-* policies are the trap. They evict only keys that carry a TTL, and WordPress object cache entries mostly carry none, so under memory pressure a volatile-lru instance stops evicting and starts refusing writes instead. The noeviction policy does that on purpose, which is a legitimate choice when somebody is watching the alert and a bad one when nobody is.

redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_policy'

Configuration note

Check the policy before you blame the cache for anything. A dedicated instance expects allkeys-lru or allkeys-lfu; a volatile-* policy on a cache whose keys have no TTL behaves like noeviction and starts failing writes at the ceiling.

Persistence is the other half of the restart question. Redis can snapshot to disk or append every write to a log, so a restarted instance can come back warm. That is useful during a rolling deploy, and it is why sites running Redis never measure their cold start.

The key prefix is the only wall between two sites on one server

Two stores on one Redis server share a keyspace, and nothing in Redis separates them unless the client does. WordPress drop-ins provide a prefix and a database number for exactly this purpose, and both are set in wp-config.php:

// One store per prefix and database number.
define( 'WP_REDIS_PREFIX', 'store-a:' );
define( 'WP_REDIS_DATABASE', 1 );

Set those before the second site is deployed, not after.

Then there is the flush. wp_cache_flush() is documented as clearing the cache, and on a dedicated instance that is all it does. Core hands the call to the drop-in, and the drop-in empties the Redis database it was configured to use. If two sites share that database, both go cold. If they share the server but sit on different database numbers, only the site that asked goes cold.

What makes this dangerous is who calls it. Any plugin may call wp_cache_flush() when it finishes an import, activates a module, or saves a settings screen, and a CSV product import on a busy store will do it more than once. WordPress 6.1 added wp_cache_flush_group(), which asks the backend to drop a single group, and a drop-in advertises whether it can through wp_cache_supports( 'flush_group' ). When the backend cannot, the group flush degrades to a full flush, which is the case you have to plan for.

If you cannot isolate the instances, the mitigation is not technical: find out which plugins flush the cache on activation, and run imports when a cold store is survivable.

A full-page cache decides which of these differences you will ever notice

Put the comparison on the axes that decide it, and answer them for your own host before you provision anything.

AxisMemcachedRedis
Survives a restartNo. Always a cold site.Optional, via snapshots or an append log.
Behaviour at the memory ceilingLRU eviction, silent.Whatever policy you set; wrong policy refuses writes.
Largest single value1 MB by default, per item.Far above anything WordPress writes.
What a flush clearsThe whole server's items for that client.The selected database, whoever else uses it.
Same host or a network hopEither, but a hop is a hop.Either, and the hop costs the same.
Shared with other workloadsRarely, because it is a cache only.Common: queues, sessions, rate limits.

The network question is worth more respect than it usually gets. Every uncached request reads alloptions and dozens of smaller keys, and each read is a round trip. On a test box running WordPress 6.6 on PHP 8.2 with Redis on the loopback interface, a get of a 40 KB alloptions entry came back in well under a millisecond. Against an instance 12 milliseconds away, no cache tuning changes the 12 milliseconds. Keep the cache on the same host, or size the site for the latency you have.

The shared-with-other-workloads row is the one people discount and later regret. Redis brings lists, sets and atomic counters, which is why the queue, the session store and the rate limiter end up in the same instance as the cache. The object cache only ever stores a blob, so none of that capability helps it, and an out-of-memory event now takes the storefront down with everything else.

Then the page cache. With a full-page cache in front, an anonymous visitor usually never touches the object cache at all. What is left is logged-in users, cart and checkout, the REST API and every request that misses the page cache. If the goal is faster TTFB for anonymous traffic, the object cache backend is the wrong lever. Page cache first, then object cache, then query work.

An object cache hides a slow query rather than fixing it

That ordering leads to the limit of everything above: an object cache makes a slow query invisible, not fast. The first request pays for the query and every later request reads a value, so the dashboard improves and the query is still there, still missing an index. A deploy, a restart, or an import that flushes puts every request back on the database in the same second.

A test I trust more than a benchmark: flush the cache, load the page once with the query log enabled, then load it again warm. If the cold page is several times slower and the warm page is fine, you have a query problem with a cache in front of it. A site that becomes fast when Redis is warmed is a site with a query problem waiting for the next flush. Warm it deliberately, and treat the gap between the two numbers as the size of the debt.

There is a maintenance asymmetry too. Memcached on the same host asks almost nothing of you: no persistence to verify, no policy to reason about, nothing to tune. Redis gives you more control and more surfaces to get wrong, and you get to pick which failure you can diagnose on a Sunday.

What to do on Monday

  1. Measure the cold start before you migrate. Flush, then time the homepage with the query log on, and write both numbers down.
  2. Check where the cache lives. Same host or a network hop. Fix a hop before you compare backends.
  3. List everything else on the instance. If anything other than the object cache is on it, set a prefix and a database number per site today.
  4. Read the eviction policy. Use allkeys-lru for a dedicated cache, and move off any volatile-* policy holding keys with no TTL.
  5. Find the largest value in the cache, starting with alloptions. Anything near a megabyte is a problem on Memcached.
  6. Grep the codebase for wp_cache_flush(), so you know who can empty the cache on a Tuesday afternoon.

Tomas Lindqvist · @tlindqvist

Hardening and operations

Threat models plugins, audits capability and nonce handling, and builds the observability that tells you an integration broke before a customer does.

Continue

Related in Hosting & Infrastructure