Skip to main content

WooCommerce slows down under concurrency, not under load

·1092 words·6 mins
Tech WooCommerce WordPress Performance Scaling

There’s a specific category of WooCommerce performance problem that only appears after you’ve crossed a certain traffic threshold, and it looks nothing like what most tuning guides prepare you for.

The usual advice covers caching layers, image optimization, and database indexes — all valid, none of it where I’ve actually felt the most pain. The real problems were in plugins doing work that nobody had bothered to bound or defer: work that looked harmless at five requests per second and fell apart at twenty.

Load and concurrency are different problems. A thousand users over an hour is a very different problem from a hundred users in the same thirty seconds. Most plugins are written for the first scenario, and most of us test for it too.

What 7.4 seconds of TTFB actually looked like
#

Illustration for What 7.4 seconds of TTFB actually looked like One WooCommerce site I maintain runs at about 2.1 seconds TTFB under normal conditions. We hit a seasonal spike — nothing dramatic, roughly double the usual volume — and TTFB jumped to 7.4 seconds. PHP workers pinned at 90%. Checkout was essentially unusable.

Nothing had been deployed — not the server, not the schema. This is the part that makes concurrency bugs genuinely disorienting: the environment looks identical to how it looked when everything was fine, so your first instinct is to look for something that changed, and you won’t find it.

The root cause turned out to be two plugins working against us at the same time:

  • A currency conversion plugin doing per-request price recalculation instead of caching converted values
  • A search plugin triggering partial index rebuilds during peak hours rather than deferring them

Each added a few hundred milliseconds individually, but under concurrent checkout traffic those costs stacked. We also found about 38 MB of autoloaded options bloating every PHP bootstrap — a problem that’s invisible at low traffic because the memory hit is constant, not variable, but which compounds badly when you’re trying to serve forty simultaneous requests.

The fix was less dramatic than the diagnosis: disabled dynamic price recalculation and switched to cached conversion values, moved index rebuilds to a low-traffic window, cleared the autoload bloat. TTFB settled back around 2.3 seconds.

The bottlenecks that don’t show up until 100k+
#

Illustration for The bottlenecks that don’t show up until 100k+ That incident made me rework the checklist I use when auditing WordPress and WooCommerce installs. The standard checklist finds standard problems. The issues below only surface when traffic gets dense enough for concurrency to expose them:

  • Action Scheduler queues growing faster than they clear — a slow, invisible drain that compounds over hours
  • Autoloaded options above 1 MB — anything in wp_options with autoload = yes gets pulled into memory on every single request, before your page does anything
  • wp_postmeta lookups degrading once the table crosses a few million rows and query patterns shift
  • WooCommerce session table writes stacking during micro spikes — each active cart creates a write, and they don’t always serialize gracefully
  • Object cache hit ratios collapsing during bursts — the cache looks healthy in steady state but falls apart when twenty users hit the same page simultaneously and all miss before the first one warms the key
  • Third-party scripts injecting render-blocking time per request — on this site, the two offending plugins alone accounted for 200–400 ms each under concurrent load
  • Plugins doing per-request lookups that should be batched or cached once
  • wp-cron triggering during checkout when concurrency spikes — particularly nasty because cron runs piggyback on incoming web requests by default
  • Slow queries buried inside transient regeneration logic — not the query you’re watching, but the one that fires when a cached value expires under load

In every case the mechanism is the same: an operation that’s cheap when it runs occasionally becomes expensive when it runs thousands of times per minute with no queue, no lock, and no back-pressure.

The audit checklist I now run at scale
#

Illustration for The audit checklist I now run at scale When I’m reviewing a site approaching or past 100k monthly visitors, these are the checks I run that most standard audits skip:

☐ Check autoloaded options size — query wp_options WHERE autoload = 'yes'
   and look for anything above ~900 KB total (that's the threshold I use;
   the right cutoff will depend on your server configuration)
☐ Inspect wp_actionscheduler tables for stuck tasks or queues not clearing
☐ Profile wp_postmeta queries with Query Monitor + slow query log together
☐ Disable WooCommerce cart fragments where you don't need real-time cart counts
   (fragments fire an AJAX request on every page load for logged-out users)
☐ Move index rebuilds and data generation jobs outside peak windows
☐ Review every third-party script in a waterfall chart — not just for size,
   but for whether it's render-blocking
☐ Test with object caching disabled temporarily to expose what the architecture
   actually depends on
☐ Run a 24-hour slow query log to catch intermittent spikes that don't show
   in a point-in-time profile
☐ Stress test checkout at concurrency above 10 simultaneous sessions

One check deserves its own note: test with object caching disabled. It sounds counterintuitive, but it’s one of the most useful things you can do. If the site becomes unusable without Redis or Memcached, that tells you something important about the architectural assumptions baked into your plugins. Caching should be a performance optimization, not the thing holding the site together.

The framing I’ve come around to
#

Illustration for The framing I’ve come around to The currency plugin was lightweight by the vendor’s own description, and the search plugin had good reviews and reasonable benchmarks — both claims accurate at moderate single-user traffic. My first instinct when TTFB spiked was actually to look at the server: I spent an embarrassing amount of time checking whether a recent PHP version bump had introduced a regression before I even thought to look at what those two plugins were doing per-request under concurrent load. The vendor documentation almost convinced me the currency plugin couldn’t be the problem. It wasn’t lying; it just wasn’t measuring the right thing.

That gap — between “performs well in testing” and “holds up under concurrency” — is where most WooCommerce scaling problems actually live. You can’t always spot it by reading plugin code or documentation; you find it by profiling under realistic traffic patterns, and sometimes you only find it when the traffic arrives.


Side note: I’m currently enabling ModSecurity with the OWASP Core Rule Set on Apache, running in detection-only mode to baseline false positives before switching enforcement on. Worth mentioning because WAF rules at the wrong threshold can themselves add latency under load — so that’s the next thing I’ll be watching on this same site.

Related

Debugging a persistent WordPress backdoor
·1009 words·5 mins
Tech WordPress Security CMS
A WordPress site was reinfecting itself after every cleanup. The culprit was a self-healing backdoor in mu-plugins that reconstructed itself from an encoded payload stored in the database. Here is how I found it, killed it, and what I missed the first time.
Dissecting a WordPress compromise: from obfuscated code to hardened infrastructure
·1166 words·6 mins
Tech WordPress Security Infrastructure
A WordPress site on our infrastructure was compromised via an abandoned theme. This is a walkthrough of how I found the malicious code, what it was actually doing, and the harder question I still can’t fully answer: whether any real damage was done before we found it.
The mailflow rule I wrote quarantined a client's shipping invoice — here is what I missed
·1023 words·5 mins
Tech Email Infrastructure Governance Deliverability
A mailflow rule meant to catch spoofed mail started blocking legitimate transactional email from a third-party shipping provider. The regex logic was sound in isolation. The infrastructure gap it exposed was not.