Skip to main content
WordPressAugust 25, 20266 min read

How to Keep a WooCommerce Checkout Up on Black Friday

Ten times the traffic barely troubles a WooCommerce catalogue and takes the checkout down. The reason is that the catalogue is served from a page cache while the cart, the checkout and the account pages cannot be, so every one of those requests boots WordPress and hits the database. This guide walks through what WooCommerce itself excludes from caching, which cookies and AJAX endpoints have to be routed past the cache, where the cart and the order actually get stored, and how to turn the checkout ceiling into a number you can size instead of a surprise you discover on the day.

Point ten times your normal traffic at a WooCommerce catalogue and it will barely notice. Point the same traffic at the checkout and the store falls over. Once you know which requests can be served from a cache and which cannot, you know exactly what to size, and you stop spending the week before a sale tuning things that were never the constraint.

The catalogue is nearly free, the checkout never is

A full page cache serves a product or category page straight from disk or memory. PHP never starts, the database is never touched, and the ceiling is whatever your web server can push in bytes per second.

WooCommerce is explicit about which pages cannot work that way. Its caching guidance names Cart, My Account and Checkout as pages that have to be excluded, because what they render is specific to one customer and one cart. Every request to those three boots WordPress with every active plugin and issues real queries.

Peak catalogue traffic is therefore a bandwidth problem, and peak checkout traffic is a PHP worker and database problem. Only one of the two is cheap to raise.

What else never comes from the cache

Three more request types escape the page cache and are easy to miss.

WooCommerce has its own AJAX endpoint, reachable as /?wc-ajax=<action> or /wc-ajax/<action>. It exists so that front end AJAX does not have to load the admin the way admin-ajax.php does, and the documentation that introduced it says plainly that caching plugins must exclude it.

The Cart and Checkout blocks talk to the Store API, a set of public REST endpoints built for customer facing cart and checkout functionality. What they return belongs to one shopper, so a cache in front of them is a data leak rather than an optimisation.

Finally, any shopper carrying a WooCommerce cookie has to be routed past the cache. The three that matter are woocommerce_cart_hash, woocommerce_items_in_cart and wp_woocommerce_session_, the last being the key WooCommerce uses to find that customer's cart in the database. There is a second reason to respect them. For logged out visitors WooCommerce ties nonce generation to the session, so a cached page handed to the wrong visitor can carry a nonce that will not validate.

On nginx, one map and two directives cover it.

# /etc/nginx/conf.d/woocommerce-cache-skip.conf
map $http_cookie $wc_skip_cache {
    default                       0;
    "~*wp_woocommerce_session_"   1;
    "~*woocommerce_items_in_cart" 1;
    "~*woocommerce_cart_hash"     1;
}
# inside the PHP location block of your site
fastcgi_cache_bypass $wc_skip_cache;
fastcgi_no_cache     $wc_skip_cache;

fastcgi_cache_bypass stops the response being taken from the cache and fastcgi_no_cache stops it being written to the cache. You want both. Setting only the first serves fresh pages to cart holders and then stores one of those personalised pages for everybody else.

Where a checkout request actually goes

Since WooCommerce 2.5 the cart lives in a dedicated table rather than in options or transients. The cookie is wp_woocommerce_session_ plus the WordPress cookie hash, the table is wp_woocommerce_sessions with your own prefix, and the row is written on shutdown. WooCommerce 10.1 tightened this further, storing sessions for logged in users only in that table rather than duplicating them into user meta, with expiry defaulting to two days for guests and seven for logged in customers, capped at thirty, and the cleanup jobs moved onto Action Scheduler.

Orders land somewhere else again. High-Performance Order Storage has been the default for new installations since WooCommerce 8.2, and it writes to wc_orders, wc_order_addresses, wc_order_operational_data and wc_orders_meta instead of the old posts and postmeta pair.

A checkout is therefore a read, a modify and a write against the session table, followed by a burst of inserts across four order tables, all of it inside a PHP process that no cache will ever spare you.

Turn the ceiling into a number

The useful arithmetic is the simplest one available. The number of PHP workers kept busy is the arrival rate multiplied by the average request duration.

Measure the average duration of a completed checkout POST on your own store, in seconds. Call it D. If you expect R checkout requests per second at peak, then R multiplied by D workers are busy on average. At 20 requests per second and a 0.9 second checkout, that is 18 workers occupied by checkout alone, before a single product page, mini cart refresh or payment callback is served.

Compare that number against pm.max_children in your PHP-FPM pool. If they are close, queueing starts, latency climbs, durations get worse, and the two numbers chase each other upward. Leave real headroom rather than matching them.

The mini cart tax

Cart fragments are the AJAX call that keeps a mini cart count correct without a page reload, and every refresh is an uncached request. Before WooCommerce 7.8 the script was enqueued on every page of a WooCommerce store whether or not a mini cart was on screen. Since 7.8 it is enqueued when the Cart Widget is actually rendered, or when something else declares it a dependency. If a theme or plugin has pulled it back onto every page, the woocommerce_get_script_data filter is the documented way to limit where it runs.

Turn the rate limit on before the sale

The Store API ships with rate limiting built in and disabled by default. The default allowance once enabled is 25 requests per 10 second window, and there is a separate switch under WooCommerce, Settings, Advanced, Features labelled for the Checkout block and Store API, which applies a limit of 3 requests per 60 seconds to the place order flow specifically.

// wp-content/mu-plugins/store-api-rate-limit.php
add_filter( 'woocommerce_store_api_rate_limit_options', function () {
    return [
        'enabled'       => true,
        'proxy_support' => true, // required if a CDN or load balancer fronts the store
        'limit'         => 25,
        'seconds'       => 10,
    ];
} );

Proxy support is off by default, and that default is a trap behind a CDN or load balancer. Without it every request appears to arrive from the proxy address, so the limit is either shared by your entire customer base or effectively meaningless. Turn it on only when you trust the forwarding headers reaching your origin.

The week before

Load test the paths that cannot be cached and ignore the ones that can. A test that hammers the homepage tells you about nginx. A test that adds to cart, refreshes fragments and posts a checkout tells you about the thing that will actually break.

Two cheap read only queries are worth having on a dashboard for the day itself.

-- how many carts are alive right now
SELECT COUNT(*) FROM wp_woocommerce_sessions;

-- orders in the last hour, using the date_created index on the HPOS table
SELECT COUNT(*) FROM wp_wc_orders
WHERE date_created_gmt > UTC_TIMESTAMP() - INTERVAL 1 HOUR;

The first tells you how many shoppers are past the cacheable part of the store. The second tells you whether they are still getting through. When the first climbs and the second flattens, the checkout is the bottleneck and no amount of cache tuning will help.

If you would rather have this measured and sized before the sale than diagnosed during it, our WordPress speed optimization work covers exactly this ground, and server setup and optimization handles the PHP-FPM and database side of the same ceiling. The WordPress performance audit checklist is a good companion for the rest of the stack.

Talk to the engineer who will own your stack.

No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.