How to set up MilliCache on a WooCommerce store

Philipp Philipp 20 min read
Illustration of a four step WooCommerce cache setup: toggles, cart and product tiles, cache flag chips reading shop, product and a wildcard, ending in a step marked Cached

This is the working companion to what a WooCommerce shop can do with MilliCache. That post explains why each setting exists and what it buys you. This one is the checklist: what to check, what to set, and how to prove it worked.

Work through it once, in order. Most of it is checking rather than typing, and the checks are what make the settings safe.

Before you start#

You need MilliCache installed and active, with its Redis connection working. The plugin’s status screen confirms both. Nothing here needs MilliCache Pro except the buckets in step 5, which says so.

One command does all the checking in this guide:

terminal
 1curl -sI https://yourshop.com/product/some-product/ | grep -i x-millicache

It prints the MilliCache headers for that page. hit means the page came from the cache, miss means it was just built and stored for next time, bypass means it was deliberately not cached. If you prefer clicking to typing, the same headers are in your browser’s developer tools Network tab, and the MilliCache extension reads them for you in Firefox.

Turn on debug mode in the plugin’s Cache settings before you begin. Every response then carries an X-MilliCache-Reason header naming the rule that made the decision, which turns every surprise in this guide into a one-line answer. Step 6 lists the reasons you are likely to see. Turn debug mode off when you are done.

The MilliCache Header Inspector reporting an edge HIT on bunny.net DE1 for millipress.com, TTFB 32 ms, 282 ms saved against a 314 ms origin response, with edge tags post:34 and home
The MilliCache Header Inspector visualizes the X-MilliCache response headers

Step 1: Find out what changes between shoppers#

Out of the box, MilliCache plays it safe. A visitor whose cookies differ from everyone else’s gets their own cached copy of every page, and WooCommerce’s cart, checkout and account pages are not cached at all.

Nothing in this guide switches those protections off blindly. Each step removes one kind of variation, after you have checked that your shop does not need it. So the first job is finding out what actually changes between two shoppers.

Start with a product page#

  1. In a private window, add a product to the cart, then open any product page.
  2. Note everything on the page that is about you: the cart count or total, prices, recently viewed products, a greeting.
  3. Open View Source and search for those values. Use View Source rather than the inspector: it shows the HTML exactly as the server sent it, before JavaScript changed anything.

If the mini-cart in the browser says 1 but there is no 1 in the source, your theme fills the cart in after the page arrives. That is a good sign, but it is not the answer yet.

The answer comes from comparing two shoppers. Open a second private window, give it a different cart, and open the same product page. Save the page source from both windows and compare them with a diff tool; diffchecker.com works in the browser. If you prefer a terminal, the whole test is five lines, with 123 and 456 standing for two product IDs from your admin:

terminal
 1curl -s -c anna.txt -b anna.txt -o /dev/null "https://yourshop.com/?add-to-cart=123"
 2curl -s -c ben.txt  -b ben.txt  -o /dev/null "https://yourshop.com/?add-to-cart=456&quantity=2"
 3curl -s -b anna.txt -D anna.headers https://yourshop.com/product/some-product/ > anna.html
 4curl -s -b ben.txt  -D ben.headers  https://yourshop.com/product/some-product/ > ben.html
 5diff anna.html ben.html

Either way, the question you are answering is:

Does the page change because these two shoppers are different?

Do not guess the answer from the kind of theme you have. A block theme, a “modern” theme or a cart that uses JavaScript can still put shopper data into the page, because WooCommerce, the theme and every installed extension all get to add to it.

What a difference means#

Two pages that match are the easy case. Two that differ need reading. On our test shop, running WooCommerce’s default theme, the diff came back with four lines: the cart total in the header and the item count in the mobile footer bar. Both are about the shopper, and both are elements that theme replaces with fresh values by JavaScript after every page load, through what WooCommerce calls cart fragments. Yours might do the same, or might not, and the diff tells you which.

A difference like that can be shared, as long as the refresh really happens on that page. Anything your theme does not replace, and anything that identifies the shopper, cannot.

Check the headers as well. If one response sets a cookie or redirects and the other does not, the server treated the two shoppers differently, however alike the pages look.

Two shortcuts look tempting and do not replace this. Opening the page in a second private window and checking that it does not show the first window’s cart is a fine sanity check, and step 6 includes it, but it only sees what the browser displays. A cart embedded in the page as data that nothing shows on screen would pass it. And comparing cache keys tells you nothing at all, because MilliCache builds the key from the request, never from the page. Once the cart cookies are ignored, two shoppers share a key by definition.

Or let an agent run it#

The comparison is two downloads and a diff, which is the kind of job an AI agent with shell access does well. This is for a tool that can run commands on your machine, such as Claude Code, Codex or an editor agent, not for a chat window. Paste it in with your shop’s address:

prompt
 1I run a WooCommerce shop at https://yourshop.com and want to know whether its
 2catalog pages and its checkout are shopper-independent, so that one cached copy
 3can be shared between shoppers with different carts.
 4
 5Use curl with two separate cookie jars, anna.txt and ben.txt. Give each jar a
 6different cart with GET requests to /?add-to-cart=<product id>, using two
 7different in-stock products and quantity 2 for one of them. Then fetch these
 8URLs with each jar, saving body and headers separately:
 9
10- one product page
11- one product category page
12- /checkout/ (both carts must be non-empty; an empty cart redirects to /cart/)
13
14For each URL, diff the two bodies and the two header sets, ignoring Date.
15Report every difference verbatim, then classify each one as
16(a) shopper data: cart contents, prices, names, addresses, emails, order or
17    customer IDs, or anything else that belongs to one shopper,
18(b) an element the theme visibly refreshes by JavaScript after load, such as a
19    mini-cart updated by WooCommerce cart fragments or the Store API,
20(c) a timestamp, nonce or other generated value.
21Do not call a page safe to share if any (a) difference exists, or if you could
22not tell (a) from (b). Tell me if either response sets a cookie or redirects.
23Only ever send GET requests.

Read the diff it reports, not just its verdict. Sorting differences into those three kinds is the part that needs a human, and the prompt is written so the agent has to show its evidence.

If your agent can also talk to your WordPress site, over the REST API or an MCP connection, MilliCache can join in. Since version 1.8 it exposes WordPress Abilities for the things an assistant is most often asked to do: report the cache status and the reason a page was not cached, clear the cache for a post or URL, and export, back up, reset or restore the plugin’s settings. MilliCache Pro extends that to cache entries, rules, preloading and the edge cache, so the same agent can read your cookie lists, see which cookie split a page, register the rules from steps 3 and 4, and confirm the hit in step 6 without anyone opening the settings screen.

You now know which of two cases you are in: your catalog pages are shared, or they are shopper-specific. Step 2 has a setting for each.

This is the step that decides your hit rate, the share of visits the cache can answer without waking WordPress. The two settings involved sound alike, so be clear on what each one means. Both live in the Cache section of the MilliCache settings.

Ignored Cookies means “this cookie never changes the page, act as though it is not there”. Visitors carrying it share cached pages with everyone else.

No-Cache Cookies means “if a visitor has this, do not use the cache for them at all”. Every page they open is built fresh.

A cookie on neither list sits in the middle: visitors carrying it get their own private cached copies. Nothing leaks that way, and it is where nearly every WooCommerce hit-rate problem comes from.

If your catalog pages are shared#

MilliCache cookie settings for a shared catalog: No-Cache Cookies holds wp-*pass* and comment_author_*, while Ignored Cookies holds _*, sbjs_*, wp_woocommerce_session_*, woocommerce_cart_hash and woocommerce_items_in_cart

Two different carts produced the same usable page, so the cookies that identify those carts have no reason to split your cache. Ignore all of them:

wp-config.php
 1define( 'MC_CACHE_IGNORE_COOKIES', [
 2	'_*',                         // the default, keep it
 3	'sbjs_*',                     // WooCommerce order tracking
 4	'wp_woocommerce_session_*',   // the shopper's session
 5	'woocommerce_cart_hash',
 6	'woocommerce_items_in_cart',
 7] );

Or paste the same five values into the Ignored Cookies field, one per line. A shopper with a full cart and a first-time visitor now receive the identical cached page.

If your catalog pages are shopper-specific#

MilliCache cookie settings for a shopper-specific catalog: the two WooCommerce cart cookies moved up into No-Cache Cookies, leaving only _* and sbjs_* in Ignored Cookies

The page really does change once someone has a cart, so the cookies behind that change must not be ignored. Have them switch the cache off instead:

wp-config.php
 1define( 'MC_CACHE_IGNORE_COOKIES', [
 2	'_*',
 3	'sbjs_*',       // still safe to ignore, and still worth it
 4] );
 5
 6define( 'MC_CACHE_NOCACHE_COOKIES', [
 7	'wp-*pass*',                  // the defaults, keep them
 8	'comment_author_*',
 9	'woocommerce_items_in_cart',  // no cache while the cart has something in it
10	'woocommerce_cart_hash',
11] );

Those two cart cookies exist only while the cart has something in it, so visitors who are just browsing still get cached pages, and on most shops that is the large majority of traffic. The session cookie is left alone on purpose: a shopper who empties their cart keeps it, and with it a private copy, until the session expires. That costs a little hit rate and keeps every page correct.

Three things that catch people out#

Name the cookies instead of using a wildcard. woocommerce_* misses the session cookie, and *woocommerce_* would also catch cookies you have not checked, such as woocommerce_recently_viewed, which the old Recently Viewed Products widget on classic themes reads on the server. The lists above name exactly the cookies you need.

The constants replace the defaults, they do not extend them. Defining either constant in wp-config.php throws away the built-in list, and the constant also overrides whatever is in the settings screen. That is why _* and wp-*pass* are repeated above: leave them out and you silently lose protections you had.

WooCommerce is rarely the only thing setting cookies. Consent managers, A/B tools, affiliate trackers and analytics scripts all add their own, and any of them splits your cache the same way. Browse a few pages, open the cookie list in your browser’s developer tools, and ask one question of each: could this change what my server puts in the page? Analytics and attribution cookies usually cannot and belong in Ignored Cookies, but check rather than judge by the name; an A/B tool or a consent manager can change what the server renders. Anything that switches currency, language or region does, and needs step 5 instead.

Step 3: The checkout#

By default MilliCache never caches the cart, the checkout or the account pages, because WooCommerce marks them off-limits and MilliCache respects that. For most shops that is the right place to stop, and you can move on to step 4.

It is also where the test from step 1 usually says to stop. The classic checkout, the one older themes build from a shortcode, puts the order summary into the page on the server, so its two versions will always differ. The candidates are shops whose checkout is a generic frame that loads the cart, addresses and totals afterwards through the Store API. On our test shop, WooCommerce’s default theme with the Checkout block, the diff between two guest carts showed only the header mini-cart, which that theme refreshes after load, and nothing about either shopper in the page.

So run the same comparison on the checkout before you switch anything on: two windows, two different carts, both page sources and headers, one diff. Identical pages are good evidence that the checkout is shopper-independent; read whatever differs before treating that as enough. The companion post walks through that comparison and what to look for in more detail.

If the comparison holds, add this to your theme’s functions.php:

functions.php
 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 matter beyond the diff. is_wc_endpoint_url() keeps the override away from the order confirmation and pay-for-order pages, which live under the checkout page and are personal by definition. set_grace( 0 ) stops the page being served past its hour: a checkout carries values with an expiry date, and a page can be identical for every shopper and still go stale.

Check it: two guest loads of the checkout with a cart in it, and the second one says hit. An order confirmation page says bypass.

Step 4: Logged-in customers#

Out of the box, MilliCache never serves a logged-in visitor from the cache. That is the right default: nobody can be handed a page that was built for someone else.

It is a default, not a wall. The rule behind it is deliberately left open to be overruled, the same way step 3 overrules WooCommerce on the checkout, so you can cache logged-in visitors for exactly the roles you have tested.

Whether you should is a matter of arithmetic. If logged-in visitors are a rounding error, skip this step. If they are a third of your traffic, a third of your traffic is currently uncached, and no amount of cookie tuning in step 2 will touch it. Wholesale shops, membership stores and B2B catalogs are the usual cases.

The deciding question is the same one as everywhere else in this guide: does your server put anything personal into these pages? On many shops a logged-in customer browsing the catalog sees exactly what everyone else sees. If that holds for you, cache them:

functions.php
 1millicache()->rules()->create( 'mysite:cache-for-customers' )
 2	->order( 10 )
 3	->when()
 4		->is_woocommerce()
 5	->and()
 6	->when()
 7		->user_role( 'customer' )
 8	->then()
 9		->do_cache( true, 'catalog pages are identical for customers' )
10	->register();

Name the roles you have actually checked rather than opening it up to “logged in” in general. Administrators and shop managers see admin bars, edit links and draft products, none of which belongs in a cache.

Check it with two different customer accounts and one admin:

same product page, three visitors
 1anna   miss, then hit   Key: 3134d4081aebcb91cea4505c1556edbf
 2benny  miss, then hit   Key: 116b599899b6ad147e547a4b95ebd20c
 3admin  bypass           Reason: Logged-in user

Read two things off that. Anna and Benny are both cached now, and their keys differ, because WordPress’s login cookie is part of the page’s identity: every logged-in visitor gets a private copy, and none of them can be handed somebody else’s. And the admin still bypasses, because the rule named one role.

Private copies sound expensive and are not, because identical pages share one stored body. In the run above that is three cache entries holding two actual pages.

One thing not to do: do not put the WordPress login cookie into Ignored Cookies to reduce the number of copies. That is the one change that would let two logged-in customers share a page, and it is how a shop shows one customer’s account details to another.

Step 5: Pages that come in a few versions#

Some choices legitimately change the page for everyone who makes them: the currency, the language, the country a regional store switcher picks, the variant of an A/B test. What to do depends on where that choice lives.

If it is in the URL, you are done. A language prefix like /de/ or a ?currency=EUR parameter is already part of the cache key, so each version gets its own copy without any setting. WooCommerce’s own geolocation does this on purpose: its “page caching support” mode adds a v= parameter to the URL that carries the visitor’s location.

If it is in a cookie, leave that cookie on neither list from step 2. It must not be ignored, because the page genuinely differs, and No-Cache would switch caching off for every shopper who made the choice, which for a currency cookie means every international customer. On neither list, the cookie’s value becomes part of the cache key, and MilliCache keeps one copy per value by itself: euro shoppers share a euro page, dollar shoppers share a dollar page. For most shops that is the whole setting.

The MilliCache Pro Rules Builder with a Set Bucket action reading the CDN country header, so each country gets its own cached copy
Cache Bucket in the MilliCache Pro Rules Builder

A cache bucket is the deliberate version of the same idea. Picture the cache as a filing cabinet with one drawer per page. A bucket adds a second drawer for the same page, labelled with a value you chose: /product/wheelset/ gets a DE drawer and a GB drawer, MilliCache opens the drawer that matches the visitor, and everyone with the same value shares what is in it. The difference from leaving a cookie on neither list is that you name the value, and you can take it from anywhere in the request, not only from a cookie.

You need a bucket when the choice does not arrive as a clean cookie. The common shop case is taxes, shipping estimates or prices by country. WooCommerce works out the country from the visitor’s IP address, nothing about it is in the URL or in a cookie, and a cache that does not know about countries shows a visitor in London the prices and taxes it built for a visitor in Berlin. If a CDN sits in front of the shop, it names the country in a request header, and one rule turns that into a bucket:

MilliCache Pro → Rules Builder
 1Condition: header cdn-requestcountrycode exists
 2Action:    set_bucket name="country" token="{header.cdn-requestcountrycode}"

That is bunny.net’s header; the companion post has Cloudflare’s and the setting it needs. The same shape works with a cookie, set_bucket name="currency" token="{cookie.currency}", and is worth it when the plugin’s cookie holds more than the plain code, or when you want the version to show up by name in MilliCache Pro’s Cache Entries browser.

Use buckets only for values with a handful of possible options, such as country or currency codes, languages or experiment variants. A session ID, or any value a visitor can make up, would give every visitor their own copy of the shop.

Buckets need MilliCache Pro. A rule that decides which cached page to look for has to run before WordPress starts.

Step 6: Verify the whole thing#

Walk through the shop once with debug mode on. These are the expected results for a shop whose catalog pages are shared, with steps 2 and 3 applied:

What you doExpected
Load a product page twicehit the second time
Load it with ?utm_source=test on the endhit, the same cached page
Add something to the cart, reload the product pagehit
Open the same product page in a second private window with an empty carthit, and its mini-cart stays empty
Open the checkout as a guest, twicehit the second time
Open an order confirmation page (/checkout/order-received/…)bypass
Open any page while logged in as an adminbypass
Open the cart pagebypass

When something says bypass and you expected otherwise, X-MilliCache-Reason names the cause:

ReasonWhat it means
DONOTCACHEPAGE constant is trueWooCommerce marked this page off-limits. Normal on the cart, checkout and account pages.
Logged-in userYou are logged in. Test in a private window, or see step 4.
Skip cache for no-cache cookiesThe visitor carries a cookie from your No-Cache list. Expected with the shopper-specific setup from step 2 and a full cart.
Setting cookie: ...This response was handing the visitor a new cookie, so it was not stored. Usually a one-off on the request that starts a session.
Non-200 response codesA redirect or an error, not a normal page.
Non-GET/HEAD requestA form submission. Never cached, and cannot be.
REST API requestNot a page view. This is how cart data reaches the browser.

One trap while testing by hand: with an empty cart the checkout redirects to the cart page, so you get a redirect and a status that tells you nothing. Put something in the cart before testing the checkout.

If your hit rate is still low#

  1. A cookie is splitting your pages. The most common cause by a wide margin. Repeat the cookie review in step 2, especially for consent and A/B tools added after launch.
  2. Your catalog pages are shopper-specific and many visitors hold a cart. Working as configured, but every shopper with a cart is uncached. This is the case where a theme change pays for itself.
  3. Much of your traffic is logged in. See step 4, which turns that from a dead end into a rule.
  4. Your cache lifetime is too short. If pages expire faster than visitors arrive, almost every visit rebuilds. Raise it and let invalidation handle freshness, which is the next section.

Keeping the shop fresh#

A short cache lifetime is not how you keep prices and stock current. MilliCache clears pages when their content changes: edit a product and its page, its category pages and the shop page are dropped while the rest of your catalog stays warm. An incoming order does the same through the stock change, with nobody touching the editor.

What is not automatic is a change that affects everything at once, such as a storewide sale. Label your catalog pages once:

functions.php
 1add_filter( 'millicache_flags_for_request', function ( $flags ) {
 2	if ( function_exists( 'is_woocommerce' ) && is_woocommerce() ) {
 3		$flags[] = 'woo:catalogue';
 4	}
 5	return $flags;
 6} );

Then clearing the catalog, and only the catalog, is one command:

terminal
 1wp millicache clear --flag="woo:catalogue" --expire

On a multisite network add --url=https://yourshop.com, otherwise the command is network-wide and a bare flag name matches nothing. Keep the --expire, and make it your habit. The companion post explains why deleting pages at peak traffic is the risky option and what MilliCache Pro does about it.

The WordPress command palette filtered by woo:catalogue, offering the MilliCache actions "Clear cache for flag" and "Expire cache for flag"
Or just clear cache entries from the Command Palette (⌘K), Admin-Bar or Settings page.

That is the whole setup. For the reasoning behind these choices, and what each one buys you, the companion post covers four things a WooCommerce shop can do with MilliCache that most caches cannot. The configuration reference documents every setting named here.

And if a diff leaves you unsure, or your shop does something this guide did not foresee, ask. We read every question on our Discord and in the GitHub community, bugs go to the issue tracker, and anything you would rather not post in public can go to . Every shop teaches us something, and this guide gets better with each one.

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.

Leave a comment