The cart that emptied itself: the CloudFront cookie prefix that zeroed WooCommerce sales
Published on June 18, 2026

The symptom: full popup cart, empty cart page
A WooCommerce store running behind CloudFront, with nginx + WAF on an EC2 instance as the origin. A customer added a product: the side mini-cart, updated via AJAX, showed the item just fine. But opening /cart/ or /checkout/, WooCommerce answered "Your cart is empty".
The impact was directly on revenue: no order could be completed. For a transactional store, this isn't a UX bug — it's the operation being down.
The partial diagnosis: the right place, for the wrong reason
The first diagnosis pointed at CloudFront caching — and it was right to point there. But the reading of the details was wrong on two points that completely changed the solution:
Wrong cache policy: it was assumed the distribution used the managed Managed-CachingOptimized policy. It was actually UseOriginCacheControlHeaders-QueryStrings — which respects the Cache-Control the origin sends. The difference is crucial, as we'll see.
Dedicated per-path behaviors: the proposal was to create separate behaviors for /cart/, /checkout/ and /my-account/, marking them non-cacheable. Not viable: the plan had a 5-behavior limit and all of them were already in use.
CloudFront caching was on the path of the problem. But the fix wasn't to create more behaviors — it was to make the origin speak the language the cache policy was already listening for.
Root cause, layer 1: the origin sent no Cache-Control
The UseOriginCacheControlHeaders policy respects the Cache-Control header from the origin response to decide what to cache and for how long. The problem: nginx sent no Cache-Control on any response — not for assets, not for pages. WordPress, by default, also doesn't send cache headers suitable for a CDN.
Without the header, the behavior was undefined: the CDN could decide to cache a dynamic cart page, serving the same response to different users. But on its own, that still didn't explain the empty cart. The second layer was missing.
Root cause, layer 2: the CloudFront Function ate the session cookie
There was a CloudFront Function running on the viewer-request event, whose job was to normalize the cache key by filtering cookies — keeping only those that matter for caching and dropping the rest. The whitelist kept cookies with the woocommerce_ prefix.
The detail that broke everything: the WooCommerce session cookie isn't called woocommerce_session — it's wp_woocommerce_session_HASH, with the wp_ prefix. It didn't match the whitelist. As a result, the Function removed the HttpOnly session cookie before the request reached the origin. Even when nginx received the request, WordPress didn't see the session cookie and created a fresh, empty session. That's why the cart "emptied" on the next page.
// CloudFront Function (viewer-request) — cookie whitelist for the cache key
// BEFORE: the wp_woocommerce_session_ cookie did NOT match
function keepCookie(name) {
return name.startsWith('woocommerce_');
}
// The real session cookie was: wp_woocommerce_session_1a2b3c... (wp_ prefix)
// → removed before reaching the origin → WordPress rebuilt an empty sessionThe fix, layer 1: correct Cache-Control in nginx
In the location / of the WAF server block, we now set Cache-Control explicitly — with no-store for transactional routes and a bypass whenever there's a session/login cookie:
# nginx (origin) — Cache-Control per route + cookie bypass
location / {
proxy_hide_header Cache-Control;
set $page_cache "public, s-maxage=3600, max-age=300";
# Transactional routes are never cached
if ($request_uri ~* "^/(cart|checkout|my-account)") {
set $page_cache "no-store, no-cache, must-revalidate";
}
# Any active session (login or cart) bypasses cache
if ($http_cookie ~* "wordpress_logged_in_|woocommerce_session_|woocommerce_cart_hash") {
set $page_cache "no-store, no-cache, must-revalidate";
}
add_header Cache-Control $page_cache;
}For static assets, the opposite — long, immutable cache, dropping the Vary header that hurts CDN efficiency:
# nginx (origin) — static assets
location ~* \.(css|js|jpg|jpeg|png|gif|svg|woff2?|ico)$ {
proxy_hide_header Vary;
proxy_hide_header Cache-Control;
add_header Cache-Control "public, max-age=31536000, immutable";
}Why detect by path AND by cookie?
Path detection (/cart/, /checkout/, /my-account/) is the most robust: it guarantees those routes are never cached, whether or not the customer has a cookie. Cookie detection covers the rest — any page viewed by a user with an active session must not serve cached content belonging to someone else.
The fix, layer 2: add the wp_woocommerce_session_ prefix to the whitelist
The CloudFront Function fix was one line — add the wp_woocommerce_session_ prefix to the whitelist of preserved cookies:
// CloudFront Function (viewer-request) — corrected whitelist
function keepCookie(name) {
return name.startsWith('woocommerce_') ||
name.startsWith('wp_woocommerce_session_'); // <-- the real session cookie
}Careful: a published CloudFront Function is global — it affects every distribution using it. The change must be reviewed and validated before going LIVE, not tested in production by accident.
Validation
nginx config test and reload (running in a container):
sudo docker exec waf nginx -t
sudo docker exec waf nginx -s reloadCloudFront invalidation — first a purge of the affected routes, so you don't wait for the old TTL to expire:
aws cloudfront create-invalidation \
--distribution-id EXXXXXXXXXXXXX \
--paths "/cart/*" "/checkout/*" "/my-account/*"The result, layer by layer:
Before → After:
/cart/ Cache-Control : (none) -> no-store, no-cache, must-revalidate
/checkout/ Cache-Control : (none) -> no-store, no-cache, must-revalidate
/my-account/ Cache-Control : (none) -> no-store, no-cache, must-revalidate
Normal pages Cache-Control : (none) -> public, s-maxage=3600, max-age=300
Assets Cache-Control : (none) -> public, max-age=31536000, immutable
wp_woocommerce_session_ cookie : removed -> passed to origin
Functional cart : NO -> YESFull flow validated end to end: add product → /cart/ shows the items. In the response headers, the confirmation: cache-control: no-store and x-cache: Miss from cloudfront on the cart routes.
Lessons
1. The WooCommerce session cookie uses the wp_ prefix. It's wp_woocommerce_session_HASH, not woocommerce_session. Any CloudFront Function (or WAF/CDN rule) that filters cookies by prefix must include wp_woocommerce_session_ explicitly.
2. Transactional routes must be no-store by path. /cart/, /checkout/ and /my-account/ must never be cached. Path detection is more robust than cookie detection — it doesn't depend on the customer already having a session.
3. Two stacked problems need two fixes. Even with Cache-Control fixed at the origin, if the Function removes the session cookie WordPress rebuilds an empty session. Both layers had to be correct at once — fixing only one would give the false impression the fix "didn't work".
4. Behavior limit? Control from the origin. When the CDN plan caps the number of behaviors, the way out isn't dedicated per-route behaviors — it's controlling caching via Cache-Control headers at the origin, which the correct cache policy already respects.
This same theme of a cookie behind a CDN showed up here before — in an invisible 401 loop with auth_basic on wp-login. The difference is that there the problem was a header being required; here it was a cookie being dropped.
Conclusion
When infrastructure, CDN and application interact, the bug rarely lives in a single layer. The empty cart wasn't "WooCommerce's fault" nor "CloudFront's fault" in isolation: it was the sum of a silent origin (no Cache-Control) and an over-zealous Function (dropping the right cookie). The fix was tiny on each end — but it only worked because both ends were handled together.