Object Caching with Redis on WordPress

Somewhere in the middle of every WordPress performance discussion, someone says just add Redis, in the tone of a person recommending vitamins. Sometimes they are right and the site gets meaningfully faster. Sometimes nothing changes except that there is now a Redis server to maintain. The difference is entirely predictable in advance, and it comes down to understanding what an object cache actually does — which takes about four paragraphs.

What an object cache is (and what it is not)

Every WordPress request assembles itself from dozens of database queries: options, the current post, its metadata, terms, user data, plugin settings. WordPress wraps all of this in an internal layer called WP_Object_Cache. By default, that cache lives in PHP memory and evaporates the instant the request ends. Request number two runs all the same queries again.

A persistent object cache gives that layer somewhere durable to live. With Redis behind it, the options, post objects, and query results computed during one request are still there for the next one, served from memory in well under a millisecond instead of recomputed through MySQL. Transients get the same upgrade automatically: with a persistent object cache present, WordPress stores them in Redis instead of bloating the wp_options table.

Crucially, this is not page caching. A page cache (WP Rocket, Cache Enabled, your host's Varnish or NGINX layer) stores finished HTML and skips PHP entirely — it is faster than Redis will ever be, for the requests it can serve. The object cache exists for everything the page cache cannot serve:

  • Logged-in users (page caches bypass them almost universally)
  • Anything with a cart, a session, or personalization — which is to say, WooCommerce
  • wp-admin, which is never page-cached and where your team spends its day
  • Cache-miss requests, where the page still has to be built before it can be stored
  • REST API and AJAX endpoints that change per user

The two caches are teammates, not rivals. Page cache for anonymous visitors, object cache for everyone else. You want both.

Do you actually need it? A one-minute decision rule

Redis pays for its own complexity when a meaningful share of your traffic is uncacheable at the page level. In practice:

Site typeVerdictWhy
Brochure site or blog, all-anonymous traffic, good page cacheSkip itNearly every request is served as static HTML; Redis would accelerate only the small remainder
WooCommerce or any e-commerceYes, almost alwaysCarts and checkouts defeat page caching; sessions and rate-limited stock queries love Redis
Membership site, LMS, bbPress/BuddyPressYesLogged-in traffic is the product
Editorial team of 5+ living in wp-adminYesAdmin screens get noticeably snappier, which compounds across a team's day
Site with heavy plugin stack and slow TTFB even on cache missesProbablyExpensive repeated queries (looking at you, unindexed meta lookups) become cache hits

One honest caveat: if your database and PHP run on the same small server and your queries are already fast, Redis shaves milliseconds you may not feel. The sites that feel the difference are the ones where MySQL is doing real repeated work — measure your uncached TTFB before and after, and let the number settle the argument.

The setup, in order

Three pieces: a Redis server, a PHP extension that can talk to it, and a WordPress drop-in that routes WP_Object_Cache through it.

1. The Redis server

On managed hosting, this is a checkbox or a support ticket — most performance-oriented hosts (including anything running your site alongside a provisioned Redis instance) hand you a host, port, and possibly a password, and you can skip to step 2. On a VPS you administer yourself:

apt install redis-server

Then two non-negotiable pieces of hardening in /etc/redis/redis.conf, because a Redis server open to the internet is a breach with extra steps:

  • bind 127.0.0.1 — listen only on localhost (or use a unix socket, which is also marginally faster)
  • requirepass with a long random password, even on localhost, if the server hosts anything multi-tenant

And two lines of capacity policy that most guides omit and most incidents involve:

  • maxmemory 256mb — a sane starting ceiling for a typical single site; without it Redis will happily grow until the kernel's OOM killer executes something you cared about
  • maxmemory-policy allkeys-lru — when the ceiling is hit, evict the least-recently-used keys. This is the correct policy for a cache. The default, noeviction, makes writes fail when memory fills, which surfaces as bizarre intermittent WordPress errors that nobody attributes to Redis for a week.

2. The PHP extension

Two options exist: phpredis (a compiled C extension) and Predis (a pure-PHP library). Use phpredis; it is faster, and the fallback exists mainly for hosts where you cannot install extensions. On your own server:

apt install php8.3-redis (match your PHP version), then restart PHP-FPM. Verify with php -m | grep redis.

3. The WordPress side

Install the Redis Object Cache plugin by Till Krüss — the de facto standard, and what most hosts' documentation assumes. Before activating the connection, put the configuration in wp-config.php (above the stop editing line):

define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PASSWORD', 'your-long-password');
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_PREFIX', 'examplecom_');
define('WP_REDIS_MAXTTL', 86400);

Two of these deserve explanation, because they prevent the two classic Redis-on-WordPress incidents:

  • WP_REDIS_PREFIX (the modern equivalent of the older WP_CACHE_KEY_SALT): if two sites — or your production and staging copies — ever share one Redis instance without distinct prefixes, they will read each other's cached objects. The symptom is production showing staging's options, or one site's users appearing on another. Set a unique prefix per site, per environment, always, even when you think the instance is dedicated. Cloned sites forget.
  • WP_REDIS_MAXTTL: WordPress writes many cache entries with no expiry. A max TTL of a day means nothing lives forever, so stale data self-heals and memory pressure stays bounded even if a plugin misbehaves.

Then enable it — the plugin copies its object-cache.php drop-in into wp-content/:

wp redis enable

And confirm:

wp redis status should report Status: Connected and the client (ideally PhpRedis). In the dashboard, Settings → Redis shows the same. From this moment, every option read, post lookup, and transient flows through Redis.

Verifying it actually works (the step everyone skips)

Connected is not the same as helping. Two checks:

Hit rate

After a day of real traffic, run redis-cli info stats and look at keyspace_hits versus keyspace_misses. Hits divided by (hits + misses) is your hit rate. A healthy WordPress object cache has a high hit rate; the plugin's dashboard widget computes it for you. A hit rate that stays low usually means something is flushing the cache constantly — often a backup or security plugin calling wp_cache_flush(), findable by temporarily deactivating suspects and watching the rate recover.

Query count

Install Query Monitor, load a heavy page twice, and compare the database query count on the second load against what you saw before Redis. On a plugin-heavy site it is normal to see the number drop from a few hundred to a few dozen. That delta is the entire value proposition, made visible.

If you want to watch it breathe, redis-cli monitor streams every operation live. Do not leave it running on a busy production site — it costs real throughput — but ten seconds of it is a genuinely good way to internalize what the cache is doing.

Operational habits that keep it boring

  • Flush on deploy, not on schedule. After deploying code that changes data shapes (plugin updates, especially WooCommerce majors), run wp cache flush once. There is no reason to flush on a cron; a cache that gets emptied nightly is a cache that spends every morning cold.
  • Know your failure mode. If Redis dies, the plugin degrades gracefully: WordPress falls back to the database and gets slower, not broken. This is the correct default — but verify it once by stopping Redis on staging and confirming the site still loads. Surprises belong in staging.
  • Watch memory once a month. redis-cli info memory, check used_memory_human against your maxmemory. If you are consistently at the ceiling with a good hit rate, grant more memory; if you are at the ceiling with a bad hit rate, something is writing garbage — look for a plugin storing uncacheable per-user data with no TTL.
  • The alloptions caveat. WordPress caches all autoloaded options as one large key (alloptions). If that blob is huge — the multi-megabyte wp_options problem — every write to any autoloaded option rewrites the whole blob, and Redis amplifies the churn. Redis is not a cure for a bloated options table; clean the table too. The two fixes compound nicely.

Common failure smells, diagnosed

  • Status: Not connected after setup — almost always the PHP extension missing (php -m | grep redis) or a password mismatch. Check wp redis status output before anything else.
  • Changes not appearing on the site — someone edited the database directly (or via a script that bypasses WordPress APIs) while the old value sat in cache. wp cache flush resolves it; the lesson is to make writes through WordPress so cache invalidation happens automatically.
  • Wrong site's content appearing — shared Redis without unique prefixes. Fix the prefix, flush, and go add a prefix to every other site on that instance while the adrenaline is fresh.
  • Intermittent write errors under load — maxmemory reached with noeviction policy. Set allkeys-lru.

The honest summary

Redis object caching is one of the rare WordPress optimizations that is both genuinely effective and essentially maintenance-free once configured with the four settings that matter: a memory ceiling, an LRU eviction policy, a unique key prefix, and a max TTL. For a WooCommerce store or any site where humans log in, it is close to mandatory. For a fully page-cached brochure site, it is a rounding error wearing an ops burden.

Set it up in twenty minutes, verify the hit rate instead of assuming, and then — this is the good part — mostly forget it exists. The best infrastructure is the kind you stop thinking about.

Photo: Nicolas Foster on Pexels