# WooCommerce Cache Exclusions Decide What Gets Stored, Not What Gets Served

> An exclusion decides what a page cache stores, not what it serves. Set WooCommerce exclusions before the master switch, and see why /cart catches too much.

- Published: 2026-09-23
- Updated: 2026-09-23
- Author: xSpeed Cache Team
- Tags: WooCommerce, Caching, Checkout, Tutorial, WordPress, platform:woocommerce, fix:cache-exclusions
- Canonical: https://xspeedcache.com/blog/cache-woocommerce-without-breaking-checkout/

---

Updated September 2026

Our page cache ships seventeen default excluded URL patterns and twenty default excluded cookies, read from [`CacheModule.php`](https://plugins.svn.wordpress.org/xspeed/trunk/includes/modules/Cache/CacheModule.php) on 23 September 2026, and it keeps a stored page for 168 hours unless you change that. Those two numbers decide what a mistake costs. A cart page written to disk before you set your exclusions is served to strangers for a week.

The order people expect is: turn caching on, load the shop, see what breaks, add exclusions. The order that works is the reverse, and the reason is mechanical rather than cautious. An exclusion rule governs what gets **stored**, and almost nothing in the serving path re-reads it. This guide sets a WooCommerce store up in the order that cannot leak a basket, with real menu paths and shipped defaults. Every step works on the free tier.

## Quick Summary: What to Do Before the Master Switch

| If you want… | Do this | Why |
|:---|:---|:---|
| A store that never leaks a cart | Set exclusions, then enable caching | Stored pages are never re-checked against later rules |
| Custom or renamed checkout pages covered | Add their paths to the URL exclusion list | WooCommerce's flag reads three page IDs, not URLs |
| The header cart total to stay correct | Keep the cart cookie in the cookie list | Cookie rules reach the web server; URL rules do not |
| To undo a bad cache | Purge, fix the rule, purge again | Editing a rule leaves pages already on disk in place |
| Geolocated prices and caching together | Use Geolocate (with page caching support) | Plain geolocation varies on what the key cannot see |

## Three Layers Answer a Cart Request, and They Know Different Things

A file-based page cache is not one decision. It is three, taken at different distances from WordPress, and each layer knows less than the one behind it. This shape is common to every page cache that writes HTML to disk, and it is why exclusion rules behave as they do.

![Three serving layers stacked from web server to PHP, each labelled with the rules it can read](https://xspeedcache.com/images/blog/woocommerce-cache-three-serving-layers.webp)

| Layer | When it answers | What it can read | What it cannot read |
|:---|:---|:---|:---|
| Web server | Before PHP starts | One bypass cookie name, a hardcoded cookie floor, the user-agent list | Your URL exclusions |
| Drop-in | Before WordPress loads | A baked cookie pattern, a baked user-agent pattern, the lifetime | Your URL exclusions |
| WordPress | The full request | Everything, including `DONOTCACHEPAGE` and the URL list | — |

Our [`advanced-cache.php`](https://plugins.svn.wordpress.org/xspeed/trunk/includes/advanced-cache.php) carries exactly five baked-in values: a cookie pattern, a user-agent pattern, the default lifetime, the edge headers and the hit log path, verified from source on 23 September 2026. No URL list is among them, because there was never meant to be one. A URL exclusion stops the page being **written**, and if the write never happens no serving layer needs the rule.

That design has one consequence worth planning around. Add `/checkout-2` to your exclusions today and every copy stored yesterday stays on disk, served by a layer that has no idea the rule exists, until it expires or you purge.

> Cookies work the other way. When PHP decides a visitor must not be served from cache it sets a `wordpress_no_cache` cookie, so the web server can enforce the whole rule list by testing one name. Our source states the limit plainly: that only covers visitors PHP has seen at least once.

## What WooCommerce Already Does, and Where It Stops

WooCommerce defends itself. `WC_Cache_Helper::set_nocache_constants()` defines `DONOTCACHEPAGE`, `DONOTCACHEOBJECT` and `DONOTCACHEDB`, and `prevent_caching()` merges no-store headers onto the cart, checkout and account pages, per [the class source](https://plugins.svn.wordpress.org/woocommerce/trunk/includes/class-wc-cache-helper.php) fetched on 23 September 2026. It reads those three pages from the `woocommerce_cart_page_id`, `woocommerce_checkout_page_id` and `woocommerce_myaccount_page_id` options.

Our comparison of [twelve caching plugins and what each does about a cart](https://xspeedcache.com/blog/woocommerce-caching-plugins/) covers which honour that flag. Two gaps survive whichever you pick:

- A second checkout, a one-page checkout plugin or a custom thank-you route is not one of the three pages, and gets nothing.
- The flag fires during a WordPress request. A page already on disk is served without one.

### The geolocation setting that changes the answer

WooCommerce's **Default customer location** control offers *Geolocate* and *Geolocate (with page caching support)*, per [`class-wc-settings-general.php`](https://plugins.svn.wordpress.org/woocommerce/trunk/includes/admin/settings/class-wc-settings-general.php) read the same day. Plain geolocation varies the page by visitor country while the cache stores one copy under one key, so the first shopper's country is served to everyone.

The page-caching variant avoids that with a redirect. `geolocation_ajax_redirect()` issues a 307 to the same URL carrying a `?v=` hash of the location, so each country lands on a distinct cache key, and `update_geolocation_hash()` stores a matching `woocommerce_geo_hash` cookie for an hour. If your store prices or ships by country, pick that mode before enabling caching.

## The Default `/cart` Rule Matches More Than Carts

A bare pattern with no glob characters is a substring test against the path, so `/cart` matches `/cart`, `/cart/items` and anything else containing those five characters. That is deliberate: the source comment says the entry carries no trailing slash precisely so it catches both forms, which WooCommerce serves.

![A substring match diagram showing which store paths the default slash-cart pattern catches](https://xspeedcache.com/images/blog/cart-exclusion-substring-match.webp)

It also catches paths nobody meant to exclude:

| Path | Matched by `/cart` | Cached? |
|:---|:---:|:---:|
| `/cart/` | ✅ | ❌ |
| `/product-category/cartridges/` | ✅ | ❌ |
| `/blog/cartoons-we-love/` | ✅ | ❌ |
| `/product/ink-cartridge-set/` | ❌ | ✅ |
| `/shop/` | ❌ | ✅ |

An ink store loses its cartridge category to a rule written for baskets, and nothing reports an error. The fix is an anchored glob: `/cart/*` matches `/cart/items` and not `/product-category/cartridges/`. Adding any of `*`, `?` or `[` switches a pattern to anchored mode.

Inside a WordPress request the gates run in a fixed order, and that order explains a confusing symptom. Query-string rejection happens before URL exclusion, so `/cart/?add-to-cart=12` is refused as a dynamic query rather than as an excluded URL, and your rule looks like it did nothing.

![The gate order inside PHP, with the query-string check sitting ahead of the URL exclusion check](https://xspeedcache.com/images/blog/page-cache-bypass-gate-order.webp)

## Set the Exclusions First

Open **xSpeed Cache → Cache → Page Cache** in your WordPress admin, or go straight to `wp-admin/admin.php?page=xspeed#/cache/cache`. Our page cache is free, built by WPDeveloper, our own company. Work top to bottom and leave the master switch until last.

1. **Read the shipped exclusions before adding any.** Ours already carry `/cart`, `/checkout`, `/my-account`, `/wc-api`, `/edd-api` and `/wp-login`, so most stores add nothing here.
2. **Add every non-standard transactional path:** a renamed checkout, a booking route, a custom account area. One pattern per line.
3. **Check the cookie list still keeps `woocommerce_`.** That one prefix covers the cart, session and geolocation cookies.
4. **Set Cache Expiry deliberately.** The default is 168 hours, and a shorter lifetime limits the blast radius of a rule you get wrong.
5. **Now turn the master switch on.** That installs the drop-in and writes `WP_CACHE` into `wp-config.php`.
6. **Purge once, then shop your own store** in a private window, with a real basket.

For a single page the free escape hatch is the editor sidebar: it writes an `_xspeed_no_cache` postmeta that beats every global rule, registered with the REST API so a script can set it too. The pattern-based [rules and bypass engine](https://xspeedcache.com/docs/rules-and-bypass/) matching by post type, category or author is Pro. A standard store does not need it, and the ordering rule applies whichever route you take.

## Checking It Actually Worked

Start with the [free xSpeed Scan](https://xspeedcache.com/scan/). It needs no account, fetches your store from outside, and reports which cache answered and what the response headers said, which is the question this setup turns on.

If you would rather check by hand, request a page and read one header:

```bash
curl -s -o /dev/null -D - 'https://example.com/cart/' | grep -i x-xspeed-cache
```

`BYPASS` on the cart is what you want. `HIT (nginx)` or `HIT (static)` means the web server answered from disk without PHP, and on a cart page that is the leak. `HIT (php)` means the drop-in answered. [Cache hits and misses](https://xspeedcache.com/docs/hits-and-misses/) lists every value.

> One trap costs people an afternoon. `curl -I` sends a `HEAD`, and only `GET` requests are served from cache, so a `HEAD` reports `BYPASS` on every URL on the site. Send a real `GET`, as above. Our [guide to confirming a cache is working](https://xspeedcache.com/blog/check-wordpress-caching-working/) covers the headers other plugins emit.

Then log out, add an item and load a cached page. If someone else's basket appears, the diagnosis is in [a logged-out visitor seeing someone else's name](https://xspeedcache.com/blog/cached-pages-showing-logged-in-content/).

## Running This Across Stores You Did Not Build

Agency work inverts the problem. You inherit a store with caching on, an unknown rule list and pages of unknown age on disk. Purge first: that makes the rules you read the rules that apply.

`wp xspeed purge` clears every cache we own in one call, and `wp xspeed cache inventory` prints which pages are cached and how old, which finds a transactional page nobody excluded. [xSpeed Hub](https://xspeedcache.com/xspeed-hub/) runs both across a fleet. Our [WooCommerce store notes](https://xspeedcache.com/use-cases/woocommerce/) list what differs from a content site; [Free vs Pro](https://xspeedcache.com/free-vs-pro/) shows where the free tier stops.

## Where Stores Get the Order Wrong

- **Enabling the cache first and fixing it after.** The pages stored in that window outlive the fix.
- **Editing a rule without purging.** A rule change is never retroactive.
- **Testing while logged in.** Logged-in visitors are never served cached pages, so the store looks right to you and broken to shoppers.
- **Leaving plain Geolocate on.** It varies the page on something the cache key never carries.

## Frequently Asked Questions

### I added `/checkout-2` to the exclusions and it is still served from cache. What went wrong?

The copies stored before the rule existed are still on disk, and the layer serving them never reads your URL list. Purge, load the page once, confirm `BYPASS`.

### My printer-ink category stopped being cached and I never excluded it.

Check it against the default `/cart` pattern: `/product-category/cartridges/` contains `/cart`, and a bare pattern is a substring test. Change it to `/cart/*`.

### Do I still need exclusions if WooCommerce sets `DONOTCACHEPAGE` itself?

Yes. It fires during a WordPress request, and a page already on disk is served without one. It also covers only the three pages named in your settings.

### My cart total in the header is wrong for logged-out shoppers.

That is the cookie rule, not the URL rule. Confirm `woocommerce_` is still in the excluded cookie list, then check the web server layer, which only learns of visitors PHP has seen.

### I purged, and the old page came back within a minute.

Something re-warmed it: a preloader, a crawler or your own visit rebuilds the page the moment it is purged. Fix the rule first, then purge.

### Can I do this without xSpeed Cache?

Yes. Every page cache that writes HTML to disk has this three-layer shape, and WP Rocket, LiteSpeed Cache, WP Fastest Cache and WP Super Cache all ship WooCommerce exclusions in their free or bundled builds. Set them before enabling the cache, whichever you use.

### Should I cache product pages?

Cache them. They are the pages a page cache helps most. Prices varying by customer group are the exception, and belong on the per-page override.

### Does the block-based checkout change any of this?

The ordering rule does not change. The page IDs can, because a block checkout may sit on a different page from the shortcode one. Confirm which your store assigns.

### Is the cookie list safe to shorten?

Treat it as the last thing you touch. The shipped entries are prefixes of hash-suffixed real cookies, so tightening them serves shared pages to visitors who must never get one.

### My staging site is fine and production leaks.

Compare the drop-in, not the settings screen. The cookie and user-agent rules compile into `advanced-cache.php` when caching is enabled and re-bake on each save, so a site whose switch was never re-flipped after a file restore holds an older pattern.

## Conclusion: Order First, Speed Second

| Your store | Start with |
|:---|:---|
| Standard WooCommerce pages | The shipped defaults, unchanged |
| A renamed or second checkout | Its path, before the master switch |
| Prices by country | Geolocate (with page caching support) |

**What to do this week:** purge the page cache, read the exclusion list you actually have, add any missing transactional path, switch a bare `/cart` to `/cart/*` if your catalogue has words containing it, then [run the free scan](https://xspeedcache.com/scan/).

Our page cache is free. Pro adds the pattern rules engine at $29 a year on the founding price, with a fourteen-day money-back guarantee and a lifetime option; [our pricing page](https://xspeedcache.com/pricing/) carries every tier. If checkout is slow rather than stale, [optimising it](https://xspeedcache.com/blog/how-to-optimize-your-woocommerce-checkout-page/) is next.
