A customer reports that their cart contains items they did not add, or that it empties when they navigate. Occasionally something worse: they see a name that is not theirs at checkout.
This is nearly always caching, and it is not a WooCommerce fault. A cache stores one copy of a page and serves it to everyone, which is exactly right for a product page and exactly wrong for a cart.
What must never be cached
/cart/, /checkout/, /my-account/, and anything else that displays something belonging to one visitor.
Most caching plugins exclude these by default when they detect WooCommerce. The failures come from setups where that detection did not happen: a server-level cache, a CDN configured separately, or a plugin installed before WooCommerce was.
What should be cached
Product pages, category listings, the shop page, ordinary content. These are identical for every visitor and they are the bulk of the traffic.
This is why the answer is exclusions rather than disabling caching. A store with caching off is slow on precisely the pages that decide whether someone reaches the cart at all. See WooCommerce speed optimisation.
The cart total in the header
Most themes show a cart count in the header, which appears on every page: including the cached ones.
WooCommerce handles this with cart fragments: the page is cached, and a small request afterwards fills in the personal part.
Two things go wrong. If the fragment request is itself cached, everyone gets the same count. The alarming symptom. And on a slow server, the fragment request delays every page load, which is why some optimisation guides suggest disabling fragments entirely.
Disabling them is reasonable only if the header count is removed too. Leaving a count that never updates is worse than not showing one.
Check what is happening
curl -I https://example.com/cart/ | grep -i -E 'cache|age|x-cache'
A cache header indicating a hit on the cart URL is the problem, stated plainly.
Test the same way a customer experiences it: add an item, then load the cart in a private window. A cart with contents in a session that never added anything is conclusive.
Layers, and finding which one
There are usually several: a plugin, possibly a server cache, possibly a CDN, plus the browser.
Each must be told separately. Excluding the cart in the plugin achieves nothing if a CDN in front is caching the same page, and this is the common configuration where everything looks correctly set and the fault persists.
Work outside in. Clear the CDN first and retest; if the fault survives, the CDN was not it. Understanding WordPress caching layers goes over identifying them.
Sessions and logged-in customers
WooCommerce stores cart contents in a session, keyed by a cookie. Caching that ignores cookies serves a logged-in customer the version built for a logged-out visitor.
Any caching layer must be configured to bypass for the WooCommerce and WordPress session cookies. A cart that empties on navigation, or a customer who appears logged out on some pages and in on others, is this.
Object caching is different
Page caching stores finished pages; object caching stores database query results. Object caching does not produce this class of fault and is genuinely valuable for a store, because WooCommerce queries heavily.
Keep them distinct when reading advice. "Disable caching to fix the cart" nearly always refers to page caching, and applying it to object caching removes a real benefit for no reason.
Test as a customer before launch
The reliable check is a full purchase in a private window, on a store with caching enabled exactly as it will run.
Add items, change quantities, reach checkout, complete an order. Then repeat in a second private window and confirm the second session sees an empty cart.
Nearly every cart-caching fault would have been caught by those two minutes. Checkout optimisation deals with what else that walk-through reveals.
Prove the exclusions are working
Configuring exclusions is easy and confirming them is the step that gets skipped, which is why stores go live with a cached basket.
for p in / /shop/ /cart/ /checkout/ /my-account/; do printf '%-14s %s\n' "$p" "$(curl -sI "https://example.com$p" | grep -iE 'x-cache|cf-cache-status' | tr -d '\r' | head -1)" done
The first two should be cached. The last three must not be. Any hit on a basket, checkout or account page is a fault regardless of how the plugin reports its configuration.
Test with a session cookie as well, since some caches behave differently once a visitor has one. A page that is correctly excluded for a logged in customer and cached for an anonymous one with items in their basket is the most common version of this.
The fragment that has to stay live
The basket count in the header is the reason stores end up caching nothing, and the problem has a narrower solution.
Rather than excluding every page that shows it, the count can be fetched separately after the page loads, so the page itself is cacheable and only the small piece is personal.
curl -s https://example.com/ | grep -oE 'cart-count[^<]*' | head curl -s 'https://example.com/?wc-ajax=get_refreshed_fragments' | head -c 200
If that second address returns content, the mechanism is in use. If the count is rendered into the page instead, every page containing the header is personal and cannot be shared.
Getting this right is what allows a store to cache its catalogue at all, which is the difference between a shop that survives a busy day and one that does not.
Purge only what changed
Clearing everything after each product edit is common and it is why some stores are slow every afternoon.
A full clear means every page has to be rebuilt on its next request, all at once. On a large catalogue that is a substantial amount of work triggered by a one word change to a description.
wp cache flush wp transient delete --expired
Most caching layers can clear a single address or a group. Use that for a product edit, and keep the full clear for changes that genuinely affect every page, such as a theme change.
Expired stored values are worth removing separately and regularly, since they accumulate in the database and are read on every request even after they have stopped being useful. Database growth covers what else builds up.