A shopper clicks Add to cart, sees the success message, opens the cart and finds it empty. Or the header shows three items while the cart page shows none. Or items disappear after logging in, after a few minutes, or when moving from the cart to the checkout. The most explicit version is the checkout message “Sorry, your session has expired. Return to shop”.
All of these mean the same thing underneath: when the request reached the server, WooCommerce could not connect it to the cart the shopper built. Either the browser did not send the session cookie, sent it to a different host, or a cache answered the request with a copy of somebody else’s page before WooCommerce ever saw it. This guide shows you how to find which, and is written for store owners, designers and developers who run WooCommerce behind any kind of cache.
What is happening
For a guest, WooCommerce keeps the cart on the server in the wp_woocommerce_sessions table (your table prefix may differ from wp_). Each row has a session_key (the customer ID), a serialized session_value containing the cart, and a session_expiry timestamp. The browser holds only a pointer to that row, in a cookie. Three cookies matter:
| Cookie |
Contains |
Set when |
wp_woocommerce_session_ plus a hash of the site URL |
Customer ID, expiry, renewal time and a security hash, separated by || |
The first time something goes in the cart (and on order-pay pages) |
woocommerce_items_in_cart |
1 |
While the cart is not empty; removed when it empties |
woocommerce_cart_hash |
A hash of the cart contents |
While the cart is not empty; changes whenever the cart changes |
In current WooCommerce a guest session lasts two days and a logged-in session a week, renewed when it is within a day of expiring; developers can change this with the wc_session_expiration and wc_session_expiring filters. Expired rows are cleaned up by a recurring Action Scheduler job, woocommerce_cleanup_sessions, every 12 hours. Logged-in customers also have a persistent copy of their cart in user meta, which is merged back when they log in.
WooCommerce tells caches to stay away from the cart, checkout and my-account pages by sending no-cache headers and defining DONOTCACHEPAGE. Those signals only work if every cache layer in front of WordPress respects them. The two cart cookies exist largely so caches can tell a shopper with a cart from an anonymous visitor.
The “Sorry, your session has expired” message is printed when WooCommerce receives a checkout request and finds the cart empty, or the checkout nonce invalid while the cart is empty. In other words it is not really about time; it means “I cannot find your cart”.
Before you start
- Access: WordPress admin, your cache plugin settings, CDN dashboard (if any), and SSH for the database checks.
- Use a private window for every test, so your admin login cookies do not change how caches treat you.
- Do not press “Clear customer sessions” (WooCommerce > Status > Tools) as a fix. It deletes every shopper’s cart on the store.
- Backup: back up before editing server configuration or running SQL.
- Running commands on the server: SSH in, then
cd to the folder that holds wp-config.php. WP-CLI is wp or wp-cli depending on the host.
- Running commands on your computer:
curl is built into macOS, Linux and current Windows (use curl.exe in PowerShell).
Step 1: Watch the cookies in your browser
- Open a private window, open DevTools and go to Application (Chrome, Edge) or Storage (Firefox), then Cookies and your domain.
- Add a product to the cart.
- Look for
wp_woocommerce_session_..., woocommerce_items_in_cart and woocommerce_cart_hash. Note the Domain column.
- Open the cart page. In the Network tab, click the document request and check that the request headers include those cookies.
| What you see |
Likely cause |
Go to |
| No WooCommerce cookies at all after adding |
The add to cart response came from cache, or PHP output broke the headers |
Step 2, then check debug.log for “headers already sent” |
Cookies on example.com but the cart page is on www.example.com (or the reverse) |
Domain mismatch |
Step 4 |
| Cookies sent, but the cart page shows empty |
The cart page HTML is cached, or the server lost the session |
Step 2, then Step 6 |
| Cart page correct, but header count wrong |
Cart fragments not refreshing on cached pages |
Step 3 |
| Checkout says session expired after the page sat open |
Stale nonce on a cached or very old page |
Step 5 |
Step 2: Stop caches serving cart, checkout and account pages
Confirm the cache is the problem
curl -sI "https://example.com/cart/" | grep -iE "cache-control|x-litespeed-cache|cf-cache-status|x-cache|x-fastcgi-cache|age"
curl -sI -H "Cookie: woocommerce_items_in_cart=1" "https://example.com/" | grep -iE "x-litespeed-cache|cf-cache-status|x-cache|x-fastcgi-cache"
Where: your computer. Placeholders: replace example.com with your domain and /cart/ with your real cart address (WooCommerce > Settings > Advanced shows the assigned pages). Flags: -s hides progress, -I fetches headers only, -H "Cookie: ..." pretends to be a shopper with something in the cart, and grep -iE keeps just the cache headers. Run each command twice. Good output: the cart page shows no-cache in cache-control and any cache status reads MISS, BYPASS, DYNAMIC or no-cache; the home page with the cart cookie is not a HIT either, unless your cache deliberately keeps a separate copy per cart (LiteSpeed does this). Bad output: HIT on the cart page, or an age value above zero. Then find the layer that answered: x-litespeed-cache is LiteSpeed, cf-cache-status is Cloudflare, x-fastcgi-cache or a custom header usually means an Nginx cache, and x-cache is often Varnish or another CDN.
LiteSpeed Cache
The LiteSpeed Cache plugin honors WooCommerce’s DONOTCACHEPAGE signal and includes a WooCommerce integration that varies cached pages by the woocommerce_cart_hash cookie. Problems usually come from extra cart-like pages it does not know about, such as a custom quick checkout, a side cart page or a wishlist. Add their paths under Cache > Excludes > Do Not Cache URIs, and any custom cart cookie under Do Not Cache Cookies. If you also run a CDN in front, the CDN needs its own rules below.
Nginx FastCGI cache
If your server uses Nginx’s own page cache, make sure the configuration skips cart pages and shoppers with cart cookies. In the server block (a file you or your host edit, then reload Nginx):
set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/cart/|/checkout/|/my-account/|/wp-json/wc/store/") { set $skip_cache 1; }
if ($http_cookie ~* "woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_|wordpress_logged_in_") { set $skip_cache 1; }
location ~ \.php$ {
# existing fastcgi_pass and include lines stay here
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
}
What it does: each if sets $skip_cache to 1 for POST requests, any query string (which covers ?wc-ajax= and ?add-to-cart=), the WooCommerce pages and Store API, and visitors carrying cart or login cookies. fastcgi_cache_bypass stops serving a cached copy to them and fastcgi_no_cache stops saving their response. Placeholders: change /cart/, /checkout/ and /my-account/ if your page slugs differ. Test with nginx -t before reloading; good output is syntax is ok and test is successful. Also look for fastcgi_ignore_headers in the configuration: if it lists Cache-Control or Set-Cookie, Nginx is ignoring WooCommerce’s no-cache headers, which makes the skip rules above essential.
Cloudflare
Cloudflare does not cache HTML by default. If you added a Cache Rule that makes HTML Eligible for cache, add a second rule with the action Bypass cache using this expression (Rules > Cache Rules, then Edit expression):
(http.request.uri.path contains "/cart" or http.request.uri.path contains "/checkout" or http.request.uri.path contains "/my-account" or http.request.uri.path contains "/wp-json/wc/store" or http.request.uri.query contains "wc-ajax" or http.request.uri.query contains "add-to-cart" or http.cookie contains "woocommerce_items_in_cart" or http.cookie contains "wp_woocommerce_session_" or http.cookie contains "wordpress_logged_in_")
What it does: it matches the WooCommerce pages, the Store API, AJAX and add to cart requests, and any visitor carrying a cart or login cookie. Placeholders: adjust the paths if your pages use other slugs. Important: Cloudflare applies matching cache rules in order and, for conflicting settings, the last matching rule wins. Place the bypass rule below the rule that caches HTML. Good result: cf-cache-status: BYPASS or DYNAMIC on the cart page in the curl test. If you use a WordPress plugin that manages Cloudflare’s HTML caching for you, use its WooCommerce exclusion settings instead of a hand-written rule. Visitors who already received a cached copy need a purge; FG-50 covers clearing every cache layer.
Step 3: Fix cart fragments and the mini cart count
Classic themes show the cart count and mini cart using cart fragments: a script, wc-cart-fragments, that calls /?wc-ajax=get_refreshed_fragments to fetch fresh HTML for the cart widget and stores it in the browser’s sessionStorage. On each page load the script reuses the stored copy only if its saved cart hash equals the woocommerce_cart_hash cookie, and treats any stored copy older than a day as expired. That is what lets a cached page still show the right count.
Since WooCommerce 7.8, WooCommerce itself only loads this script where the legacy Cart widget is used, but many themes and plugins enqueue it on every page. The block-based Mini-Cart does not use fragments at all; it reads the cart from the Store API.
When the count is wrong, open the Network tab, filter for get_refreshed_fragments and reload. Good: status 200 with JSON containing fragments and cart_hash. Bad: no request at all (the script is missing, delayed by an optimization plugin, or not loaded on that page), a 403 (firewall), or a response served from cache. In the Console, run document.cookie and Object.keys(sessionStorage): you should see woocommerce_cart_hash in the first and keys starting wc_fragments_ and wc_cart_hash_ in the second. If the cookie is missing while sessionStorage has a hash, the stored fragments are stale and the cookie is being lost (Step 4). Performance trade-offs of fragments are in FG-56.
Step 4: Fix www, non-www and http or https mismatches
WooCommerce sets its cookies without a domain attribute by default, so the browser only returns them to the exact host that set them. A cart built on example.com is invisible on www.example.com. The cookie name itself includes a hash of the site URL. And on an https site the session cookie is marked secure, so it is never sent over plain http.
wp option get home
wp option get siteurl
curl -sI "http://example.com/" | grep -i "^location"
curl -sI "https://www.example.com/" | grep -i "^location"
Where: the first two lines on the server, the curl lines on your computer. Placeholders: use both forms of your domain. Flags: grep -i "^location" shows only the redirect target. Good output: home and siteurl are identical and use https; every other form of the address redirects in one hop to that exact URL. Bad output: no location line (that form serves the site directly, so carts split between hosts), or home and siteurl differ. Also check menus and buttons: a hard-coded link to the other host in a header template sends shoppers to an empty cart. Setting up the single redirect is covered in FG-39. Only define COOKIE_DOMAIN in wp-config.php if you deliberately share a cart across subdomains, because it also changes how login cookies behave (FG-18).
Step 5: Stop stale nonces on cached pages
A nonce is a short-lived token that proves a form came from your site. WordPress nonces are valid for between 12 and 24 hours by default. For guests, WooCommerce ties its nonces to the shopper’s session customer ID, so a nonce baked into a cached page belongs to whoever triggered that cache entry.
- Classic checkout: a bad checkout nonce gives “We were unable to process your order, please try again.” if the cart still has items, or “Sorry, your session has expired” if the cart is empty.
- Block cart and checkout: the Store API rejects the request with
woocommerce_rest_invalid_nonce and status 403 when the Nonce header does not validate.
- Any cached page with a form (add to cart with extra options, product inquiries, quick view) can carry an expired nonce once the copy is older than the nonce window.
The fix is the exclusions in Step 2, plus a page cache lifetime shorter than the nonce window for pages that embed forms. Many cache plugins default to a longer lifetime, so set it below 12 hours (10 is a common choice) or use the plugin’s option to refresh nonces.
Step 6: Check the session data on the server
If cookies arrive correctly and no cache answers the request, check whether the server-side session survives. First get the customer ID: in DevTools, copy the value of the wp_woocommerce_session_... cookie. It is URL encoded, so %7C%7C stands for ||. The part before the first || is the customer ID.
wp db prefix
wp db query "SELECT session_key, FROM_UNIXTIME(session_expiry) AS expires, LENGTH(session_value) AS bytes FROM wp_woocommerce_sessions WHERE session_key = 'CUSTOMER_ID';"
wp db query "SELECT COUNT(*) AS sessions FROM wp_woocommerce_sessions;"
Where: server SSH, site folder. Placeholders: CUSTOMER_ID is the value you extracted from the cookie; replace wp_ in the table name with the prefix that wp db prefix prints. Good output: one row whose expiry is in the future and whose size grows when you add items. Bad output: no row (the session was never saved, or something deleted it), or an error that the table does not exist. A missing table after a migration means it was not copied; WooCommerce > Status > Tools > Verify base database tables checks for and recreates missing core tables.
A persistent object cache adds another place for sessions to go wrong. WooCommerce caches session data in the object cache. If a staging copy and the live store share the same Redis or Memcached database without a unique key prefix (for example a different WP_CACHE_KEY_SALT), they read and overwrite each other’s data, and carts vanish or appear on the wrong site. Give each site its own database number or key salt, then flush the object cache with wp cache flush.
Advanced: Trace one cart through every layer with curl
A cookie jar lets you reproduce a shopper’s journey from the command line and see exactly where the cart is lost.
curl -s -c jar.txt -b jar.txt -o /dev/null -D - "https://example.com/?add-to-cart=PRODUCT_ID" | grep -iE "set-cookie|cache|location"
curl -s -b jar.txt "https://example.com/wp-json/wc/store/v1/cart" | grep -o '"items_count":[0-9]*'
curl -s -b jar.txt -D - -o /dev/null "https://example.com/cart/" | grep -iE "cache|age"
Where: your computer, in any folder (the jar file is created there). Placeholders: PRODUCT_ID is the ID of a simple, in-stock product; hover over the product in Products > All Products to see it. Flags: -c jar.txt saves cookies the server sets, -b jar.txt sends them back, -D - prints response headers to the screen, -o /dev/null discards the HTML, and grep -o pulls out just the item count from the Store API’s JSON.
Good output: the first request shows set-cookie lines for all three WooCommerce cookies and no cache HIT; the second prints "items_count":1; the third shows no cache HIT. Reading bad output: no set-cookie on the first request means a cache answered it (check its cache header) or the request was redirected (check location, then repeat with the final URL). "items_count":0 on the second means the cookie reached the server but the session was not found, which points at Step 6. A HIT on the third means the cart page itself is cached. Delete jar.txt afterward.
When to contact your host
Contact your host when a cache layer you cannot see or configure is answering cart requests (a cache status header you do not recognize, or HITs that survive every plugin setting), when the Nginx or Varnish configuration is managed for you, or when the object cache is shared between sites. Send:
- The exact URLs tested and the UTC time of each test.
- The full response headers from the curl commands above, including any Cloudflare Ray ID.
- Which cookies the browser held at the time, and whether the issue affects www, non-www or both.
- What changed recently: a new cache plugin, CDN rule, server move or staging push.
The diagnostic tools page can capture response headers for you. If carts are failing for every customer right now, start at the Emergency Room.