Your Rate Limits Are Not Working. The Object Cache Is Why.
Here is an arithmetic problem that decides whether a WordPress community site survives its own growth.
Unread notification counts get polled while a member has the site open. Say the poll runs every 30 seconds, which is unremarkable, and say you have 100,000 members. That works out at roughly 3,300 COUNT queries per second from unread counts alone, before anyone has loaded a feed, opened a directory or posted anything.
Every one of those is a real database query unless something is holding the answer in memory between requests. That something is a persistent object cache, and most sites running at that scale either have one or are about to find out why they need one.
Nothing about this is specific to any particular community plugin. It is what any platform of that shape looks like at that size without shared caching.
What “persistent” actually changes
WordPress has an object cache whether you install anything or not. The distinction people miss is that the default one is request-local.
Without a persistent backing store, wpcacheget() and wpcacheset() still work exactly as documented. The caching code you wrote still runs, and it is still correct. The value is computed, stored, used for the rest of that request, and thrown away when the request ends. The next request computes it again from scratch.
So the caching layer is not broken. It simply never gets to save you the query that mattered, which was the one on the next request.
For a small site this is completely fine. The queries are cheap, there are not many of them, and the cache would not have earned its keep. The gap opens with the number of active members, and it opens faster than most people expect because the expensive things are the ones that get polled.
The four areas that degrade
When there is no persistent object cache, these are what pay for it:
| Area | What happens |
|---|---|
| Unread counts | Recomputed on every request, for every member |
| Member directory | Result sets re-queried instead of reused |
| Feed page one | The hottest page on the whole site, recomputed per request |
| Rate limiting | Degrades, and not in the way you would guess |
Three of those four are speed problems. You can measure them, throw hardware at them, and decide how much slowness you are willing to accept.
The fourth one is different, and it is the reason this is not just a performance article.
Rate limiting is a correctness problem, not a speed problem
Rate limits count actions across requests. That is the entire mechanism: a counter goes up when someone posts, and when the counter passes a threshold within a window, the next attempt is refused.
With a request-local cache, there is nothing to count in. Each request starts with an empty counter, increments it once, and discards it. The limit is still enforced within a single request, which is to say it is enforced against nothing, because nobody floods a site by making one request.
Your settings screen says five posts per minute. Your actual behaviour is closer to unlimited. Nothing errors, nothing logs, and the admin UI shows a configured limit that is not being applied.
This is why well-built platforms check for a persistent object cache specifically in their rate limiting code rather than trusting the cache API to have done its job. If you maintain anything that rate-limits, that check is worth adding today, and worth surfacing to the site owner rather than failing quietly.
One clarification before anyone over-corrects, because these two ideas sit close together: having a persistent object cache makes your rate limits work, but it does not make the cache a security boundary. We covered that distinction in why your object cache is not a security control. The cache is where the counter lives. The enforcement decision still belongs in your code, and anything in a cache can be evicted at any moment.
The general principle is worth extracting: any feature that counts across requests is a correctness feature, not a caching optimisation. Rate limits, quota enforcement, attempt lockouts, and abuse thresholds all belong in that category. Treating them as “nice to have caching” is how they end up silently absent.
How to tell what you have
Three ways, in increasing order of directness.
WP-CLI, which is the one to trust:
wp eval 'var_dump( wp_using_ext_object_cache() );'
true means a persistent object cache is installed and active. false means WordPress is using its request-local fallback, whatever the plugins screen suggests.
Site Health, under Tools, then Info, then Caching. This is the one to point a client at, because it needs no terminal.
The presence of a drop-in. A persistent object cache installs wp-content/object-cache.php. If that file is not there, you do not have one, regardless of what is in the plugins list. An object cache plugin being active and the drop-in being in place are two different states, and they come apart more often than you would think, usually after a migration or a file-permission change.
That last failure mode is worth building into your handover checklist. A site that had a working object cache in staging and lost the drop-in during deployment shows no error at all. It just gets slower, and the rate limits quietly stop applying.
Why this bites communities harder than content sites
A brochure site or a blog can go a long way without a persistent object cache, and plenty do. It is worth understanding why community software is different, because it explains why the advice you read for a normal WordPress site does not transfer.
The expensive queries are per-member, not per-page. On a content site, the front page is the same for everyone, so a page cache in front of PHP handles almost all of the traffic and the object cache underneath rarely gets asked. On a community site, almost every page is personalised. The feed is different for each member, the unread count is by definition per-member, and the directory changes depending on who you have blocked. Page caching cannot help with any of it, so every request reaches PHP and the database.
Logged-in traffic is the normal case, not the exception. Most WordPress performance advice quietly assumes the majority of visitors are anonymous. Invert that assumption and the entire caching stack changes shape: the layer that does the work is no longer the page cache, it is the object cache, and if the object cache is request-local then there is effectively no caching layer at all.
Polling multiplies everything. A member reading a page generates one request. A member with the tab open generates a request every polling interval, indefinitely, whether or not they are looking at it. This is the mechanism behind the number at the top of this article, and it is why the load scales with sessions open rather than with pages read.
Put those together and a community site with 5,000 active members can generate more database work than a content site with a hundred times the pageviews.
What to actually install
The realistic options are Redis or Memcached, with a WordPress drop-in in front. Which one matters much less than whether one exists.
The decision that does matter is where it runs. An object cache on the same box as PHP is the common case and works well. An object cache shared across multiple application servers is what you need the moment you scale horizontally, and it is also what makes the rate-limiting correctness argument work across a fleet rather than per-server.
If you run a single server and are not planning to add another, either backend is fine and you should pick whichever your host already supports. If you are on multiple application servers without a shared object cache, your rate limits are per-server, which is a multiple of what the admin screen claims.
The measurement worth doing before you decide
You do not have to take the arithmetic on trust. Query Monitor gives you per-request query counts on any page. Load the feed, the member directory and a notifications page as a logged-in member, and note the numbers. If any single query dominates the total, fix that first and re-measure, because a cache in front of one pathological query is treating the symptom; our walkthrough on finding and fixing the slow queries killing your site covers that order of operations.
Then compute your own version of the figure at the top of this piece. Take your concurrently-active member count, divide by your polling interval, and multiply by the number of queries the count endpoint runs. That gives you queries per second from one feature. If it is in the hundreds, you have your answer. If it is in single digits, you genuinely do not need this yet, and installing Redis to feel professional is a maintenance burden you have chosen for no reason.
The honest framing is that this is a recommendation, not a requirement. A community site works without a persistent object cache. It is simply slower, and the gap widens with every active member, and one specific feature stops meaning what its settings say.
Three ways it breaks after it is working
Installing one is the easy part. Keeping it is where the recurring client work comes from, and the failures share a family resemblance: none of them raise an error.
The drop-in disappears. Covered above, and worth repeating because it is the most common. Any deployment process that syncs wp-content selectively, any migration tool, any file-permission repair can remove or orphan object-cache.php. The site keeps working. Add a check for wpusingextobjectcache() to whatever monitoring you already run, because this is not something you will notice by looking.
The cache and the code disagree about invalidation. A persistent cache means a stale value can now outlive the request that created it, which was not true before. Code that was correct with a request-local cache can be wrong with a persistent one, particularly anything that writes and then reads the same key. This is the one genuine risk of installing a cache on a site that has never had one, and it is why the change belongs on staging first even though it looks like a pure infrastructure change. The specific failure shapes are worth knowing in advance: we wrote up four object cache bugs that only appear in production, and every one of them is invisible until a persistent backend exists.
Memory fills and eviction starts. An object cache with too small an allocation quietly evicts entries to make room, and the symptom is a site that is fast at 3am and slow at peak. Check the eviction counter, not just the hit rate. A hit rate of 80 percent with heavy eviction is a cache that is being asked to hold more than it can, and adding memory is cheaper than any code change you might otherwise go looking for.
The short version
- Without a persistent object cache,
wpcache*is request-local. The caching code is correct and useless. - Unread counts, the member directory and feed page one all recompute per request.
- Rate limiting does not merely slow down. It stops working, silently, while still appearing configured.
- Check with
wp eval 'vardump( wpusingextobject_cache() );', not with the plugins screen. - Verify
object-cache.phpexists after every migration. Losing the drop-in is invisible. - Measure before installing. If your own arithmetic lands in single digits, skip it.
The reason this one is worth putting on a checklist rather than a performance backlog is the rate-limiting behaviour. Slow is a thing you notice. A limit that is configured but not applied is a thing you notice after someone has used it.