WooCommerce caching beyond the usual exclusions: carts, checkout, geolocation
Anna drops a set of pedals into her cart and keeps browsing. Ben has never been to your shop, and opens the same product page a second later. Both are served the exact same cached page, byte for byte, out of one cache entry. Anna’s cart is untouched: it lives in her WooCommerce session, which her browser identifies with a cookie, and the mini-cart on the page is filled in from there after the page arrives.
That is not what happens by default, in MilliCache or in any other cache. A cache that sees Anna’s session cookie has two safe reactions: give her a private copy of every page she opens, or stop caching for her altogether. Both are safe. Both also cost you the pages you can least afford to rebuild, because shoppers with a cart are the visitors closest to buying. What MilliCache adds is the ability to decide from evidence instead: from what your shop actually puts in the page, cookie by cookie and page by page.
None of this is theoretical. Propain build mountain bikes and sell them, along with a full bike configurator, on WooCommerce. They have been running MilliCache in production for several months and then decided to upgrade to MilliCache Pro:
Instead of moving to the cloud with load balancing, our website and bike configurator keep running on a single server thanks to MilliCache. TTFB on pages outside the edge cache went from around 1.4 s to under 350.
This post covers four things a WooCommerce shop can do with MilliCache that are hard or impossible on a conventional cache:
- Serve one cached page to shoppers with and without a cart
- Cache the checkout
- Clear a page without anyone ever waiting for the rebuild
- Show every country the right prices with a single rule
The quoted output comes from a WooCommerce 11 test shop running MilliCache 1.8, and every trick ends with the command that checks it on your own shop.
Trick 1: two shoppers, one page#
Back to Anna and Ben. With the right setup, MilliCache serves both of them the same stored HTML:
1Ben (no cart): hit Key: 5179727ec48f9bb29a8d5a598852417c
2Anna (full cart): hit Key: 5179727ec48f9bb29a8d5a598852417c
The key is the name of a cache entry. MilliCache builds it from the URL and from the cookies it has not been told to ignore, so an identical key means both visitors were served the same stored page. Anna’s cart cookies did not create a second entry, because MilliCache was told to ignore them.
The important part is not that Anna has a cart. It is whether her cart changes the HTML WordPress sends for this page.
Some themes write the cart into the page on the server. Others send a generic header or mini-cart and fill the cart in afterwards with JavaScript, through AJAX or the WooCommerce Store API. Extensions can change that in either direction. The page itself tells you which kind of shop you have.
First: does the cart change the page?#
The only way to know is to compare. Open the same product page as two shoppers with different carts and diff the two page sources, headers included. The setup guide has the step-by-step version, the exact commands, and a prompt that lets an AI agent run it for you. What you are looking for is whether either page contains anything that belongs to that shopper: cart contents, customer details, prices, recently viewed products, membership state, or anything else that should not be shared.
Expect some differences even on a shop that turns out to be fine. On our test shop the only differences between two carts were the header cart total and the mobile footer count, both of which the theme rewrites with JavaScript once the page has loaded. The rule of thumb: a difference the theme replaces before anyone sees it can be shared; one it leaves in place, or anything that identifies the shopper, cannot. The setup guide shows how to read a diff like that.
So a theme can make two very different shopping sessions produce the same usable page, or make them differ, and a plugin can flip either case. That is why the useful rule is not “WooCommerce sessions cannot be cached”. It is:
Vary the cache by a cookie only when that cookie changes something in the page that reaches the visitor.
Then: ignore the cookies that do not matter#
WooCommerce and its extensions set several cookies during normal browsing. Left alone, those cookies split one product page into many cache entries even when WordPress keeps producing the same HTML.
Once you have checked that the cart and the session do not change your catalog pages, tell MilliCache to ignore their cookies:
1define( 'MC_CACHE_IGNORE_COOKIES', [
2 '_*',
3 'sbjs_*',
4 'wp_woocommerce_session_*',
5 'woocommerce_cart_hash',
6 'woocommerce_items_in_cart',
7] );
5179727e…
7c181d78…
Anna
wp_woocommerce_session_*
The three cart and session entries are not a universal WooCommerce recipe. They are right only after you have checked that those values do not change the pages you want to share.
That is also why the list names the cookies rather than using a wildcard. The setup guide covers the cookies a wildcard would catch by mistake.
The safe configuration is not the one with the longest ignore list. It is the one that ignores only what you have shown does not matter.
Trick 2: cache pages WooCommerce told you not to cache#
WooCommerce treats the cart, checkout and account pages as dynamic and tells page caches not to store them. MilliCache respects that out of the box, and for most shops that is exactly what you want.
But “WooCommerce says this page is dynamic” and “this page contains something about the shopper” are not the same thing.
Whether the checkout contains anything about the shopper is the same question as in trick 1, with one extra warning: WooCommerce’s own Checkout block carries code to preload the cart into the page, and whether it does so on your shop depends on version and configuration. Check the page, not the block list.
So MilliCache keeps WooCommerce’s exclusion as the default, and lets you override it when you know more about your shop than a generic caching plugin can.
Test the page, not the technology#
Create two guest sessions with different carts, different products and quantities, and different shipping details if your flow allows it. Save the checkout page and its headers for both, and diff them, ignoring the date line in the headers:
1curl -s -b anna.txt -D anna.headers https://yourshop.com/checkout/ > anna.html
2curl -s -b ben.txt -D ben.headers https://yourshop.com/checkout/ > ben.html
3
4diff anna.html ben.html
5diff anna.headers ben.headers
What you find sorts into three kinds. Anything about the shopper, Anna’s address, her cart, her shipping options or her customer ID, means the page cannot be shared, full stop, and extensions add to this kind: payment providers, wholesale pricing, memberships and checkout-field plugins all put things in the page that stock WooCommerce does not. A generated value such as a nonce is nobody’s data, but it stops working after a while, which limits how long a cached copy stays usable. A timestamp only costs you hit rate.
On our test shop, WooCommerce’s default theme with the Checkout block, two carts produced checkouts that differed only in the header mini-cart, which the theme rewrites after load; the block fetched the cart from the Store API afterwards, so the page itself carried nothing that belonged to either shopper. The classic shortcode checkout renders the order summary on the server, so there the diff is never clean and the default exclusion stands.
If the page is genuinely independent of the shopper, override WooCommerce’s default:
1millicache()->rules()->create( 'mysite:cache-checkout' )
2 ->order( 10 )
3 ->when()
4 ->is_checkout()
5 ->and()
6 ->when_none()
7 ->is_user_logged_in()
8 ->is_wc_endpoint_url()
9 ->then()
10 ->do_cache( true, 'verified shared checkout response' )
11 ->set_ttl( HOUR_IN_SECONDS )
12 ->set_grace( 0 )
13 ->register();
Two lines in that rule guard against things the diff cannot show you. is_wc_endpoint_url() keeps the override off the order confirmation and pay-for-order pages: they live under the same WordPress page, so is_checkout() is true for them too, and without that line our test shop cached a guest’s order confirmation and served it on the next request for that URL. set_grace( 0 ) handles freshness, which is separate from sharing: MilliCache normally keeps serving an expired page for up to thirty days while it rebuilds in the background, which trick 3 depends on, and a page with nonces in it should not get that. The hour itself is a cautious choice, not what makes the page safe. If a future theme or extension puts shopper data into the checkout, the rule has to go.
This is also why MilliCache has no “cache the checkout” checkbox. The useful feature is starting from safe defaults and writing down the exceptions for a shop you understand.
Confirm it: open the checkout twice as a guest with something in the cart, and the second load says hit. Then open an order confirmation page (/checkout/order-received/…) and it must say bypass.
Trick 3: nobody waits for a rebuild#
The usual objection to caching a shop is stock. Someone buys the last wheelset, and the cached product page keeps saying “in stock” to the next hundred visitors.
Here is that scenario, run on purpose. A wheelset with five in stock, with its product page, its category page and the shop page all cached and warm. Then an order for two comes in, and nobody touches the admin:
1/product/wheelset/ miss
2/product-category/wheels/ miss
3/shop/ miss
All three were dropped on the spot, and the rebuilt product page said “3 in stock”. Buy the last three and it comes back “Out of stock”. The stale page does not happen.
What did not happen matters just as much: every other product page in the shop was left alone. MilliCache labels each cached page with what is on it, so a change to one product reaches that product’s pages and stops there. A shop with a thousand products stays warm while one of them updates.
Click flags to select · Hover to highlight · Enter to clear
Labelling your own pages#
The labels, called flags, are not only for products. You can attach your own and clear by it from anywhere. This one marks every catalog page, which is exactly the set of pages a price import or a storewide sale makes stale:
1millicache()->rules()->create( 'mysite:flag-catalog' )
2 ->when()
3 ->is_woocommerce()
4 ->then()
5 ->add_flag( 'woo' )
6 ->register();
Reload any shop, category or product page and the label is on it, next to the ones MilliCache added by itself:
1X-MilliCache-Flags: url:b4f4744360a4aa009c3919bdb0864ff7 1:woo 1:archive:product
If you would rather not write a rule, a filter does the same job:
1add_filter( 'millicache_flags_for_request', function ( $flags ) {
2 if ( function_exists( 'is_woocommerce' ) && is_woocommerce() ) {
3 $flags[] = 'woo';
4 }
5 return $flags;
6} );
Either way, a storewide sale is now one command instead of a full flush:
1$ wp millicache clear --flag="woo" --expire
2Success: Expired 3 cache entries.
On a multisite network add --url=https://yourshop.com so the flag is scoped to that site. Without it the command is network-wide and a bare flag name matches nothing: prefix it with the site ID instead, --flag="3:woo" for site 3, or --flag="*:woo" for every site in the network.
The part that saves your server#
The last word of that command, --expire, is the one worth knowing about. It does not delete anything.
Think about what deleting would mean. You start a sale at nine in the morning and throw away four hundred catalog pages. The next four hundred visitors each land on a page that no longer exists, so each one waits while WordPress builds it. That can turn into hundreds of full page renders inside a minute, at the busiest hour of your week, caused by the cache clear rather than by the traffic. The failure has a name, cache stampede, and a full flush at peak time is the simplest way to cause one.
--expire marks the pages as out of date instead. The next visitor to each page still gets the old copy instantly, and the fresh one is built behind them, after their page has already been sent. The visitor after that gets the new one. Nobody watches a spinner, and your server rebuilds at whatever pace real traffic asks for, rather than all at once.
The same thing happens when a page simply gets old, without you doing anything: MilliCache keeps serving it for up to thirty days past its lifetime while it refreshes in the background, so pages that quietly age out never make anyone wait either. The background rebuild relies on PHP-FPM’s fastcgi_finish_request(), which hands the finished page to the web server and lets PHP keep working. On a setup without it, the rebuild happens while the visitor waits, which is the behaviour you would have had anyway.
Or let the server do the rebuilding#
Everything above is the free plugin, and --expire is the right habit for it: nothing is thrown away, so nobody can land on a missing page.
MilliCache Pro removes the need to remember. With Cache Preloading switched on, a page that leaves the cache is queued straight away to be built again by your server, in the background, instead of waiting for a visitor to trigger it. Those four hundred deleted catalog pages become four hundred entries in a background queue that your server works through at its own pace, and the pages are warm again before most shoppers get near them.
The same happens every time you save a product. Its own page and the archives that list it are rebuilt at once, so the first customer to arrive after your edit gets a cached page rather than being the one who pays to build it.
And it can work from your sitemap, which is the answer to the morning after a five-thousand-product import: warm the entire catalog in one run, and open the shop already fast rather than letting the first hour of customers warm it for you.
Trick 4: one rule for every country#
Sell across borders and the shared page from trick 1 stops being one page, because a visitor in Berlin, a visitor in London and a visitor in New York should not see the same prices, taxes or shipping. WooCommerce works out where a visitor is from their IP address and adjusts taxes, shipping estimates and, with a pricing plugin, the prices themselves. Nothing about that decision is in the URL, and nothing is in a cookie. A cache has no way to tell the two visitors apart, so the first one to arrive decides what everyone after them sees.
WooCommerce knows this. Its own workaround is the “Geolocate (with page caching support)” setting, which redirects every visitor’s first page view to a URL carrying a location code, so that a cache can keep the versions apart. It works, at the price of a redirect for every new visitor and a ?v= on every URL they see from then on.
The better answer keeps the URLs clean: one cached copy per country, and one rule, not one rule per country. If a CDN sits in front of the shop, it tells you the visitor’s country in a request header, and that header picks a cache bucket. Think of a bucket as an edition of your shop. MilliCache keeps one edition per country, sends every visitor to the edition for their country, and everyone on that edition shares its pages. Underneath, the country code becomes part of the cache key before MilliCache looks anything up:
1Condition: header cdn-requestcountrycode exists
2Action: set_bucket name="country" token="{header.cdn-requestcountrycode}"
The {header.cdn-requestcountrycode} part fills itself in with whatever the CDN saw, so opening a fifth country next year needs no change at all, and every visitor still gets a cached page. That header is the one bunny.net sends to every origin by default. Cloudflare’s is CF-IPCountry, and it only appears once you switch on the “Add visitor location headers” transform in the Cloudflare dashboard.

The same shape covers anything else your shop legitimately serves in a few versions: a currency cookie (token="{cookie.currency}"), a language, an A/B variant. A cookie that holds a clean value gives you a copy per value even without a rule, because cookies you have not ignored are part of the cache key already. The rule earns its place when the value comes from somewhere else, as the country does, or when you want the version named so the Cache Entries browser shows it.

Buckets suit values with a short, known list: country codes, currencies, languages, experiment variants. Do not feed one a session ID or anything a visitor can invent, because every distinct value is another edition of your entire shop.
This one is a MilliCache Pro feature, for a reason worth knowing. The first three tricks all happen while WordPress is running. This one has to happen before WordPress starts, because it decides which cached page to even look for, and at that moment no theme or plugin exists yet to run your code. Rules that early have to be stored in the settings ahead of time, which is what MilliCache Pro’s Rules Builder does.
What stays protected#
All four tricks loosen a default, so it is worth being precise about which protections are defaults and which are not.
Some of MilliCache’s rules are locked, and no rule you write can override them. Only GET and HEAD requests are ever cached, so a form submission or the “add to cart” click never is. A visitor carrying a cookie from your No-Cache list, or opening a path from your No-Cache list, is never served from the cache. And a response that hands the visitor a new cookie is never stored, which is how the request that opens a WooCommerce session keeps itself out of the cache.
Everything else is a default. Logged-in visitors bypass the cache. Error pages and redirects are not stored. Cart, checkout and account pages bypass because WooCommerce marks them with DONOTCACHEPAGE, and MilliCache matches that by page rather than by URL, so renaming or translating them changes nothing. A default can be overridden on purpose: trick 2 does exactly that for the checkout only, for guests only, leaving its endpoints alone, and the setup guide shows the same move for logged-in customers. A default you have not touched behaves exactly like a lock.
Checking your own shop#
You do not need to turn anything on to see what your cache is doing. Every page your shop serves says so in its response headers, and there are three ways to read them.
The browser extension. The MilliCache Header Inspector adds a MilliCache tab to Firefox’s developer tools that reads the headers for you and explains what they mean, page after page. It also understands a CDN sitting in front of your shop, which is the situation where raw headers are easiest to misread. Firefox only for now.

Your browser’s developer tools. Works anywhere: press F12, open the Network tab, reload the page, click the very first request in the list, and read the response headers. The ones beginning with X-MilliCache are yours.

curl. If you live in a terminal, this is the fastest of the three:
1curl -sI https://yourshop.com/product/some-product/ | grep -i x-millicache
hit means the page came from the cache. miss means it was built just now and stored for next time. bypass means the page was deliberately not cached. Load a product page twice: the second should say hit. Add something to your cart and load it again: with trick 1 in place, still hit. If something says bypass and you cannot see why, switch on debug mode in the settings and the response will name the rule that made the decision.
Try it#
MilliCache does the first three tricks: the cookie settings, the checkout rule, and the labelling that lets an order clear its own pages. The setup guide walks through all of it step by step, and the configuration reference documents every setting.
MilliCache Pro adds the fourth, plus the tools that make the rest visible:
- The Rules Builder writes rules like the country one by clicking, no PHP.
- The Cache Entries browser shows which pages are cached and which cookie split them, so a tracking cookie cannot quietly cost you your hit rate.
- Cache Preloading rebuilds cleared pages for you, and warms the whole catalog from your sitemap after an import.
- Detailed Metrics puts a number on all of it.
Get new articles first
Guides, updates and release notes in your inbox. No spam.
By subscribing you agree to our Privacy Policy. Double opt-in, no spam, unsubscribe anytime.