Databases

Dropping Redis in front of a slow WordPress site

DB

A client's site did a lot of repeated, expensive WP_Query calls on every page load, category archives filtered by multiple taxonomies, that kind of thing. Page caching alone didn't help much since a lot of their traffic hit logged-in areas that page caches skip entirely, membership content behind a login wall that a page cache, by design, isn't allowed to serve to different logged-in users identically.

Why page caching wasn't enough

Page caching works by storing a fully rendered HTML page and serving that same page to subsequent visitors instead of regenerating it from scratch on every request. It's a huge win for anonymous, public content, but it's useless for anything that varies per visitor, and this client's site had a meaningful chunk of traffic doing exactly that, logged-in members browsing personalized dashboards where a shared, cached page simply isn't a valid answer. Every one of those requests was hitting the database fresh, running the same expensive taxonomy-filtered queries over and over for different visitors looking at overlapping data.

What we set up instead

We installed Redis on the droplet and used an object cache drop-in so WordPress's internal object caching, which normally resets every request, actually persists between requests. Repeated queries started hitting Redis instead of MySQL, which meant the same underlying data being requested by two different logged-in visitors within a short window only had to be computed once.

WordPress has had an internal object cache API for a long time, but by default it's a non-persistent, in-memory cache that only lives for the duration of a single request, which means it helps avoid redundant queries within one page load but does nothing across separate requests. A persistent object cache backend, Redis in our case, changes that non-persistent cache into one that actually survives between requests, which is the whole reason this made a measurable difference for a site with this kind of traffic pattern.

What made the setup fiddlier than expected

Setup was fiddlier than we expected, mostly around getting the PHP Redis extension installed correctly on Ubuntu, where the version available through the default package repository didn't match what the drop-in plugin expected, and compiling it from source was a genuinely new experience for both of us. We ended up finding a slightly newer repository that had a compatible pre-built package, which saved us from compiling anything by hand, but it took a couple of hours of dead ends to get there.

  • Confirm the PHP Redis extension is actually loaded with a quick phpinfo() check before assuming the drop-in will work.
  • Restart PHP-FPM after any change to PHP extensions, which is an easy step to forget and produces confusing "it should be working" symptoms if skipped.
  • Test object cache hits are actually happening using a debug plugin that reports cache hit and miss counts, rather than assuming it's working just because nothing errored.

The resulting speedup

The resulting speedup on the taxonomy-heavy pages was the biggest single performance win we've made on that project, noticeably larger than any of the query optimization work we'd done previously. Pages that were taking a couple of seconds to generate for a logged-in visitor dropped to a fraction of that, since the expensive part of the work, the actual database query, was now happening once and being reused rather than recomputed for every visitor hitting overlapping content.

What we're watching for going forward

Object caching backed by Redis introduces a new failure mode we didn't have before, cache invalidation. If content changes and the cache isn't cleared appropriately, a visitor can see stale data. WordPress's built-in cache invalidation hooks handle most common cases, a post being saved clears the relevant cached queries, but we're keeping an eye out for any edge case where stale data slips through, particularly around the custom taxonomy filtering this client's site relies on heavily, since that's more custom logic than the plugin ecosystem's cache invalidation hooks were necessarily built with in mind.

We're also watching Redis's own memory usage, since an object cache with no eviction policy configured can theoretically grow without bound on a site with enough unique query variations. For now, on this droplet's memory budget, that's not a practical concern, but it's the kind of thing worth checking on again once the site's traffic or content volume grows meaningfully from where it sits today.

Deciding this was worth doing at all

Before touching anything, we spent an evening actually confirming this client's specific slowness was a caching problem rather than something else, a poorly indexed query, a plugin doing something wasteful independent of caching, network latency between the droplet and visitors. Profiling first, rather than reaching for Redis because it's the trendy answer to "WordPress is slow," mattered here, since object caching solves a specific class of problem, repeated identical or overlapping queries, and would have done nothing for a site whose slowness came from somewhere else entirely.

What this would have looked like on shared hosting

Worth noting, none of this would have been possible on the shared hosting we described in an earlier post this year. Installing a system-level service like Redis and a matching PHP extension requires the kind of server access a shared hosting account simply doesn't grant. This is one of the concrete, specific paybacks of having moved this client onto a droplet earlier in the year, a performance option that wasn't even on the table before became straightforward once we had actual root access to the server.

Would we recommend this to every WordPress client

No, and we want to be clear about that. This was worth the setup complexity specifically because this client's traffic pattern, a meaningful volume of logged-in, personalized page views hitting overlapping expensive queries, made object caching a genuinely high-leverage fix. For a typical brochure site with mostly anonymous traffic, page caching alone covers the overwhelming majority of the benefit for a fraction of the setup complexity, and we wouldn't recommend a client take on the added moving part of a Redis dependency without a traffic pattern that actually justifies it.

A note on what we'd do differently next time

If we were setting this up again, we'd install the cache-hit debugging plugin before making any changes at all, to get a clear baseline reading of cache miss rates under the existing non-persistent cache, rather than only installing it after Redis was already running. Without that baseline, our "biggest single performance win" claim is based on before-and-after page load times rather than a precise measurement of cache hit rate improvement, which is a softer form of evidence than we'd like for a change we're recommending other people consider making too. Small process gap, easy to fix on the next project that calls for this.

We're also documenting the exact Redis and PHP extension versions that ended up working together on this server, since finding a compatible combination took real trial and error, and there's no reason the next person on our team, quite possibly one of us in six months having forgotten the details, should have to rediscover that same path from scratch.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients