
Full-page caching serves the exact same HTML to every visitor, and a WooCommerce cart, checkout, or my-account page is never the same twice — it renders a nonce, a session-specific cart total, and account data unique to whoever is logged in. Cache that HTML and the second visitor gets the first visitor's cart, an expired nonce, or someone else's order history baked into a static file.
Nonces are the sharpest edge here. WooCommerce embeds a fresh woocommerce-process-checkout-nonce in the checkout form on every page load, and WordPress rejects a submitted nonce once it expires or doesn't match the session that generated it. A cached checkout page ships the same nonce to everyone until the cache expires, so a portion of checkout attempts fail with "we were unable to process your order" — usually reported as random, unreproducible checkout failures because it only happens to visitors who load the page from cache.
Stale cart totals are the second failure mode. Product pages and the shop archive can be cached safely because their content is the same for every visitor, but the mini-cart icon, item count, and subtotal shown in the header are per-session state. If that header markup gets baked into a cached page, a customer who just added a product still sees an empty cart icon until the cache entry expires.
WooCommerce solves the stale-header problem with an AJAX call, not by disabling caching on every page. After any cart action — add to cart, update quantity, apply coupon — the storefront JavaScript fires a request to ?wc-ajax=get_refreshed_fragments, and the server responds with fresh HTML fragments for the mini-cart widget and updated cart count, which JavaScript then swaps into the DOM.
This is why a fully cached homepage or product page can still show an accurate cart icon: the page shell is static and cacheable, but the cart state layered on top of it arrives via an uncached AJAX request every time. The catch is that this only works if your page cache and any reverse proxy actually let wc-ajax requests through uncached — if a cache rule matches on URL prefix and accidentally captures query strings, fragment requests get cached too and the cart appears frozen at whatever it looked like when the cache entry was written.
The rule is simple: never let a full-page cache — whether it's a plugin, Nginx, Varnish, or a CDN — store the cart, checkout, or my-account pages, and never cache any request containing wc-ajax in the query string. Every caching plugin built for WooCommerce (WP Rocket, W3 Total Cache, LiteSpeed Cache) ships this exclusion by default, matching on the page slug configured in WooCommerce > Settings > Advanced, but if you're caching at the server level with Nginx fastcgi_cache or a CDN edge rule, you have to add the exclusion yourself.
Sending Cache-Control: no-cache, no-store, must-revalidate on those three page types (plus any request with wc-ajax=) is the belt-and-suspenders approach — it tells your own cache layer to skip storage and stops any downstream CDN or browser from caching the response either.
Never cache the checkout page, even for a few seconds. A cached checkout serves a stale nonce to every visitor who loads it from cache, causing order-submission failures that look random and are hard to reproduce, and in the worst case it can serve one customer's partially-filled checkout form or order-review totals to another. If you use a CDN in front of your store, set an explicit bypass rule for the checkout and my-account paths rather than relying on a short TTL.
If Nginx is doing the page caching with fastcgi_cache, set a skip_cache variable that catches the cart, checkout, my-account URIs and any wc-ajax query string, then reference it in both fastcgi_cache_bypass and fastcgi_no_cache so those requests never write to or read from the cache zone:
The POST bypass matters too — add-to-cart and checkout submissions are POST requests, and fastcgi_cache only ever caches GET/HEAD responses by default, but making the skip explicit avoids surprises if that default is ever overridden elsewhere in the config.
Redis object caching doesn't conflict with any of the exclusion rules above because it caches database query results, not rendered HTML pages. Every WordPress request runs dozens of repeated queries — options lookups, wp_postmeta reads for product data, wp_woocommerce_sessions lookups for the current cart — and an object cache stores those individual query results in Redis so PHP doesn't hit MariaDB for the same data on every request.
Because the cart, checkout, and my-account pages still execute PHP on every request — they're never served from the full-page cache — they benefit the most from Redis, since it's the layer speeding up the exact pages you excluded from page caching. A drop-in like Redis Object Cache reads WP_REDIS_HOST from wp-config.php and requires no changes to the Nginx rules you set up for the page cache.
Confirm Redis is running and reachable before installing an object cache plugin — on a CloudStick server Redis ships as a system package, so a quick redis-cli ping should return PONG from the same server before you point WordPress at it.
Confirm the split is working before you trust it in production: load a product page twice and check for an X-Cache: HIT (or your plugin's equivalent header) on the second request, then load /checkout/ twice and confirm it stays a MISS every time. Add a product to cart from two different browsers and verify each one sees its own cart count in the header — that confirms fragment requests aren't being served from a shared cache entry.
On CloudStick, the built-in WordPress caching/optimizer applies page-cache exclusions to WooCommerce's cart, checkout, and account pages automatically when it detects the plugin, and the Visual Database Manager lets you inspect wp_woocommerce_sessions directly if you ever need to confirm cart data is landing in the database rather than getting cached where it shouldn't. Get the exclusions right once and full-page caching, AJAX fragments, and Redis object caching all run together without one undoing another.

