> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# The WooCommerce Cache That Served One Customer's Cart to the Next
- URL: https://debugly.dev/woocommerce-cart-cache-leak/
- Published: 2026-10-09T12:37:00.000Z
- Updated: 2026-10-10T13:47:41.000Z
- Description: Two strangers bought different things and received each other's cart fragments, because a page cache keyed on the URL returned a cached, personalised block…
- Author: Rohit Bhadani
- Tags: WooCommerce, Ecommerce, Bug Hunt

The complaints were the kind a store dreads, customer A screenshotting a cart containing customer B's items, and the reproduction was as simple as it was horrifying: add an item, copy the page URL, open it in a clean browser, and someone else's cart fragment rendered. The store had enabled a page cache to survive a traffic spike, and the cache, keyed on the URL, had captured a page that included a personalised block, the mini cart, and was serving that block, intact, to the next visitor who requested the same URL. The cache was doing its job. The key was the whole defect.

This is the multi tenant cache collision from [the cache that returned another customer's data](https://debugly.dev/cache-key-missing-tenant-id/), wearing a storefront's clothes, and it is worth its own autopsy because the ecommerce variant has a specific trap: the page is mostly public and partly personal, and the cache cannot tell the difference.

This was a WooCommerce store behind a full page cache, and the leak was a cached fragment, not the whole session, which is what made it intermittent and believable as a glitch.

## The symptom, precisely

Anonymous visitors to the same product or category URL sometimes saw a mini cart, a greeting, or a "continue where you left off" block belonging to a previous visitor, while the rest of the page was correct. The selectivity is the fingerprint: only the personalised fragment leaked, only when the cache held a version populated by a logged in or cart holding visitor, and only until the entry expired, which made it vanish before support could look, the classic cache bug rhythm.

## Why the cache cannot see the boundary

A full page cache stores the rendered HTML per key, and the key the plugin used was the URL plus, at most, a currency or language dimension. The rendered HTML, however, contained output that depends on the session: the cart contents, the customer name, the cross sell block trained on the session's views. The cache has no way to know which bytes are public and which are personal, because the rendering already happened, and the rendering mixed the two into one document. So the first render's session bled into every later hit for that key, and the key, being the URL, is shared by strangers by definition.

This is the omitted dimension defect, where the key lacked the one axis on which the answer varies, the session, and the cache's correctness is bounded by its key, which is the rule from [cache invalidation and stale data bugs](https://debugly.dev/cache-invalidation-stale-data-bugs/) applied to identity rather than time.

## The hypotheses that died

### A session cookie mix up

First I assumed cookies were being shared, a set cookie on the wrong domain. Clean browsers with no cookies still saw the fragment, so no cookie was involved at delivery time. It died because the leak arrived in the HTML, not in a cookie.

### A database cross read

Next I checked whether the cart query could return another cart. Direct queries were clean per session. It died because the database was never consulted on the cached hit, which is the point of the cache and the point of the leak.

### A logged in cache poisoning

Then I assumed only logged in users poisoned entries. Anonymous visitors with items in a cart also personalised the page, and WooCommerce carts exist without login, so the anonymous population poisoned too. It died because the personalisation boundary is the cart, not the account.

## The fix, in order

**Exclude the personalised surface from the cached document.** The durable fix renders the personal block outside the cached HTML: the cached page carries a placeholder, and the cart fragment loads after, from an uncached endpoint scoped to the session, the same split as [the section rendering API returning yesterday's cart](https://debugly.dev/shopify-section-rendering-api-stale-fragments/) on the Shopify side, where the shell is cacheable and the volatile part is session scoped. The cache then stores only public bytes, and the key's missing dimension stops matter.

**Key or bypass on the personal axis.** Where the fragment must be server rendered, the cache must not serve it across sessions: bypass the cache for requests carrying a cart cookie, or include a per session token in the key, accepting that the cache warms per session and therefore helps less, which is the honest trade, a cache that is right and cold beats one that is warm and wrong.

**Vary the edge on the cookie that marks personalisation.** At the CDN layer, a Vary or a cookie based rule that separates anonymous from cart holding traffic stops the edge from storing the personal variant under the public key, which is the edge version of the same key discipline.

## The test that catches it

The isolation test is two browsers and one URL: populate a cart in browser A, request the URL fresh in browser B, and assert B sees no trace of A's cart, repeating across the cache's expiry to catch the warm entry. The test belongs in the store's regression suite the day the cache is enabled, because enabling the cache without it is enabling the leak with a delay, and the delay is the cache TTL.

It is also worth alerting on the impossible: a server side check that the rendered cart fragment's session matches the request's session, logging any mismatch, which turns the leak from a customer screenshot into a metric, the tripwire discipline applied to the cache.

## What I now do

A page that is partly public and partly personal cannot be cached as one document under a public key, because the cache stores the first render's session and serves it to strangers. Render the personal fragment outside the cached shell, scope it to the session, or key the cache on the personal axis and accept the colder cache.

The cache was never broken. It returned exactly what it stored, to exactly the key it was given, and the key was a URL that two strangers share. That is the whole pattern, and it is the part worth remembering.