DEV Community

Cover image for 10 practices that keep WooCommerce fast at scale
binadit
binadit

Posted on • Originally published at binadit.com

10 practices that keep WooCommerce fast at scale

Your WooCommerce store is fine until it isn't

Everything runs smoothly until a flash sale hits, or your ERP sync job starts locking tables during checkout hours. If you're running WooCommerce past a few hundred SKUs or a few thousand orders a month, you've probably already hit one of these walls. Good news: none of this requires a replatform. It's config, query discipline, and infrastructure decisions you can roll out incrementally.

Here are 10 practices that actually move the needle, ranked roughly by how fast you'll feel the impact.

1. Get sessions and cart data out of MySQL

By default, WooCommerce dumps cart and session data into wp_options and wp_woocommerce_sessions. Under concurrent load, that's write contention on tables that are already getting hammered by product and pricing reads.

Move it to Redis. Sub-millisecond lookups instead of a MySQL round trip per cart update.

// wp-config.php or a custom session handler plugin
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_DATABASE', 2);
Enter fullscreen mode Exit fullscreen mode

2. Don't conflate object cache with page cache

Object caching (Redis/Memcached) caches individual query results; product lookups, term queries, user meta. Page caching serves fully rendered HTML. They need different invalidation logic entirely.

Cart and category pages aren't fully cacheable at the page level since they're price-sensitive and user-specific, but they still benefit massively from object caching underneath. Keep them separated and you get both speed and correctness.

3. Never let full-page cache touch cart, checkout, or account pages

A cache plugin that doesn't understand WooCommerce's dynamic fragments will happily serve one customer's cart to another. That's not a performance win, that's a bug report waiting to happen.

# Nginx: never cache dynamic WooCommerce endpoints
location ~* ^/(cart|checkout|my-account) {
    proxy_no_cache 1;
    proxy_cache_bypass 1;
}
Enter fullscreen mode Exit fullscreen mode

Everything else (product pages, categories, homepage) can be cached aggressively as long as cart fragments load via AJAX post-render.

4. Stop refreshing cart fragments on pages where nothing changed

The default mini-cart AJAX call (wc-ajax=get_refreshed_fragments) is what makes point 3 possible, but it's commonly left running on every page load, blog posts included. Throttle it or disable it on pages where cart state can't change. Noticeable PHP-FPM load reduction on content-heavy stores.

5. Audit your indexes, not just WooCommerce's defaults

Default indexes on wp_postmeta and wp_wc_order_stats are fine for standard queries. Custom filters, ERP syncs, and reporting plugins introduce meta queries that often aren't indexed at all.

-- Check for missing composite indexes
EXPLAIN SELECT post_id FROM wp_postmeta
WHERE meta_key = '_stock_status' AND meta_value = 'instock';

-- If it's a full table scan:
ALTER TABLE wp_postmeta ADD INDEX idx_meta_key_value (meta_key, meta_value(20));
Enter fullscreen mode Exit fullscreen mode

Do this quarterly. Plugin updates change query patterns, and indexes don't fix themselves.

6. Queue everything that isn't the checkout itself

Order confirmation emails, ERP syncs, webhooks: none of that should run synchronously inside the checkout request. If a downstream system is slow, your customer is the one waiting.

// Defer post-order work instead of running it inline
add_action('woocommerce_order_status_completed', function($order_id) {
    as_schedule_single_action(time(), 'sync_order_to_erp', ['order_id' => $order_id]);
});
Enter fullscreen mode Exit fullscreen mode

Action Scheduler ships with WooCommerce already; use it, or push to a Redis-backed queue. Keep the request path short.

7. CDN your images, not just your HTML

Product catalogs with hundreds of high-res images are usually the biggest bandwidth drain on the whole stack.

location ~* .(jpg|jpeg|png|webp|avif)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}
Enter fullscreen mode Exit fullscreen mode

Serve WebP/AVIF where supported. This has a direct effect on mobile checkout conversion, arguably more than most backend tuning.

8. Autoscale PHP-FPM based on concurrency, not vibes

A fixed pm.max_children tuned for average traffic will choke during a flash sale and sit idle the rest of the time.

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
Enter fullscreen mode Exit fullscreen mode

pm.max_requests is underrated: it forces worker recycling and prevents slow memory leaks in plugin code from degrading performance over a day.

9. Send reporting and sync jobs to a read replica

Analytics dashboards and ERP syncs run expensive aggregate queries. Running them against your primary competes directly with checkout for connections and locks. Point them at a replica instead and isolate the load completely.

10. Load test the checkout flow, not the homepage

Homepage numbers look great because it's the most cacheable page you have. Checkout is the opposite: least cacheable, most DB-intensive, and the one that actually determines revenue. Simulate the real flow: add to cart, apply coupon, enter payment, submit.

Rollout order that won't blow up your team

  • Weeks 1-2: object cache + session offload (1-2). Zero code risk, immediate latency win.
  • Weeks 3-4: cache exclusions + CDN config (3, 7). Low risk, big TTFB improvement for anonymous traffic.
  • Month 2: query/index audit + queue migration (5, 6). Touches business logic, test accordingly.
  • Month 2-3: PHP-FPM tuning + read replica (8, 9). Do these once you have real traffic data to tune against.

Read the full original writeup here: woocommerce-fast-at-scale-managed-infrastructure-for-saas

Originally published on binadit.com

Top comments (0)