Skip to content
All articles

What Breaks First When a WooCommerce Store Gets Busy

7 min read

"WooCommerce doesn't scale" is one of those claims that gets repeated until it sounds like a fact. It isn't. Stores doing serious daily traffic run on it every day. What doesn't scale is everything built around the quiet assumption that the store would stay small — the caching setup that was fine for a brochure site, the options table nobody has looked at in four years, the plugin that runs one extra query per product on every page load.

The failure mode is also rarely dramatic. It's not the site going down; it's the admin taking eight seconds to load an order list, checkout getting slower every month, and the hosting bill creeping up while nobody can point at what changed. Here's what tends to give first, roughly in the order I'd go looking.

1. The page cache quietly stops applying

On a content site, a page cache is close to a silver bullet: the first visitor pays for the PHP, everyone else gets a static file. A store is different, because a store has carts, sessions and logged-in customers — and all three are reasons for a cache to step aside.

WooCommerce sets a session cookie as soon as someone has something in their cart, and any competent caching layer treats that as "this response is personal, don't serve it from cache". That's correct behaviour. The problem is that people measure performance in a fresh incognito window, see a cache hit, and conclude the site is fast — while a meaningful share of real traffic, the share that's actually shopping, is going straight to PHP on every single page.

Two things worth checking specifically. First, that cart, checkout and my-account are excluded from the page cache deliberately rather than by accident — a cached checkout page is a much worse problem than a slow one. Second, cart fragments: WooCommerce refreshes the little "3 items · $48.00" cart widget with an AJAX request on page load, and that request can't be cached by definition. On a theme that shows a cart widget in the header of every page, that's one uncached PHP request per pageview happening in the background, whether or not the visitor has a cart. If your product pages look fast but the server is busier than it should be, look there first.

2. The options table gets heavy

WordPress loads every row in wp_options marked autoload on every single request, before it knows what page it's rendering. That's by design and it's fine when it's a few hundred kilobytes of settings. It stops being fine when years of plugins — including ones long since deleted — have each parked their own serialized blob in there and marked it autoload.

It's worth actually measuring rather than assuming: query the total size of your autoloaded options and look at the biggest offenders by name. It's common to find a single plugin holding megabytes of cached API responses or logs in an autoloaded option, being unserialized on every request including AJAX calls and cron runs. It's one of the highest-leverage things you can fix on an old store, and it takes an afternoon.

Expired transients live in the same table and behave the same way. WordPress cleans them up lazily, which in practice means a busy store can accumulate a lot of dead rows between cleanups.

3. Background jobs pile up where nobody looks

WooCommerce runs a lot of work through Action Scheduler — emails, webhooks, subscription renewals, sync jobs from whatever else you've integrated. Every one of those actions is a database row, and completed actions are retained for a while before cleanup.

On a busy store with a few integrations, that table grows fast, and it's the kind of growth nobody notices because nothing about it is user-facing until it is. The two symptoms to watch for: a scheduled-actions table that's among the largest in the database, and a growing backlog of pending actions — which means the queue isn't draining as fast as it's filling, and the "background" work is now permanently behind. WooCommerce surfaces both under WooCommerce → Status → Scheduled Actions. If the pending count only goes up, that's a real problem wearing a harmless-looking hat.

While you're in there: if the site is still relying on WordPress's default cron — which only runs when someone visits a page — a busy store is doing that check constantly, and a quiet one isn't doing it at all. Disabling the page-triggered behaviour and driving cron from a real system cron job is standard practice for anything transactional.

4. The admin gets slow before the storefront does

This is the one that surprises people. The storefront can be perfectly quick while the order screen takes ten seconds, because they're doing completely different work: the storefront reads a handful of published products, the admin filters and sorts across every order you've ever taken.

Historically WooCommerce stored orders in wp_posts and wp_postmeta alongside blog posts, which meant order queries were meta queries against a table shared with everything else on the site. High-Performance Order Storage moves orders into their own properly indexed tables, and it's the default for new installs on current versions — but an older store that's been upgraded in place may well still be running the legacy layout, or running both in sync. It's worth confirming which mode you're actually in rather than assuming the version number settled it, and planning the migration if you haven't done it. On a store with a long order history it's the single biggest thing you can do for admin responsiveness.

The same logic applies to product data: WooCommerce maintains lookup tables so that things like filtering by price or stock status don't have to go through post meta. If those get out of sync — usually after a bulk import or a migration — you get slow, subtly wrong filtering. There's a tool to rebuild them under WooCommerce → Status → Tools.

5. Logged-in traffic has nowhere to be cached

Once you've accepted that shoppers with a session bypass the page cache, the question becomes what catches them instead. That's what an object cache is for: Redis or Memcached holding the results of the database queries that WordPress repeats on every request, so a personalised page still avoids re-running the same lookups from scratch.

On a store where a large share of traffic is logged in or carrying a cart, adding a persistent object cache is often a bigger win than any further tuning of the page cache — because the page cache had already stopped applying to the traffic that matters.

6. Search and filtering hit the database directly

Default WordPress search is a LIKE query across post content. It works. It does not work well across tens of thousands of products with attribute filters layered on top, and it degrades in exactly the way that's hardest to notice: fine on the catalogue you tested with, bad on the one that grew.

If search and faceted filtering are how people actually navigate the store — and on a large catalogue they are — that work eventually belongs in something built for it rather than in MySQL. That's a bigger change than anything else on this list, so it's last: worth doing when search is the bottleneck, not before.

Find out which one it is before you fix any of them

Everything above is a candidate, not a diagnosis. The tool I'd reach for first is Query Monitor, which will tell you, for the specific page that feels slow, how many queries ran, which were slowest, and — the useful part — which plugin or theme triggered them. Ten minutes with that on a real slow page is worth more than a day of applying general advice, including this article's.

Do it on a staging copy with real data volume, too. A store's problems are almost always proportional to how much data it has accumulated, which is precisely what a clean test install doesn't have. The bug isn't in WooCommerce; it's in the four years of history nobody was watching.