# WooCommerce: keep cart, checkout and account pages out of the cache

> A cached cart shows another shopper's basket and a cached checkout fails with an expired session. How to exclude the three pages and the WooCommerce cookies at every layer, from plugin to CDN.

Updated 2026-09-25 · WordPress & WooCommerce · HTML version: https://getreport.app/guides/woocommerce-caching-cart-checkout-and-account-pages

Page caching is the single biggest speed win for a WooCommerce store and the single easiest way to break it. Cache the product pages and the store flies; cache the cart and a shopper sees someone else's basket; cache the checkout and every order fails with "session expired". WooCommerce marks these pages as not cacheable, and caching plugins listen. The problem is every layer that does not: a host's server cache, a CDN with a "cache everything" rule, a Varnish in front of nginx. This guide shows what goes wrong, how WooCommerce signals it, and the exclusion for each layer. Plan an hour and a test purchase in two browsers.

## Quick answer

- Exclude `/cart/`, `/checkout/` and `/my-account/` (and every translated slug) from **every** cache layer: plugin, host, CDN, reverse proxy.
- Bypass the cache for any request that carries a `woocommerce_items_in_cart`, `woocommerce_cart_hash`, `wp_woocommerce_session_*` or `wordpress_logged_in_*` cookie.
- Never cache `?wc-ajax=` requests or `/wp-json/wc/store/`.
- WooCommerce already sends `Cache-Control: no-cache, must-revalidate, max-age=0` on those pages; a layer that ignores that header needs an explicit rule.
- Test with `curl -sI https://example.com/checkout/` twice: no `age` header, no `HIT` status.
- The [WooCommerce checker](https://getreport.app/tools/woocommerce-checker) fetches `/cart/` and `/checkout/` and reports which one came from a cache, with the header that proves it.

## Why a cached cart matters

A page cache stores the HTML of one response and serves it to everyone who asks for the same URL until it expires. For a product page that is exactly right. For the cart it is wrong in the worst way: the first shopper to load `/cart/` after a purge fills the cache with their basket, and every shopper for the next hour sees three pairs of the first shopper's shoes. On the checkout, the cached page contains a security token (a nonce) that was valid for the first shopper's session. When the next shopper submits the form, WooCommerce rejects the token, the page says "Sorry, your session has expired" or "We were unable to process your order", and the shopper leaves. Payment gateways that render inside the checkout fail in stranger ways, because their tokens are cached too.

The account pages leak. A cached `/my-account/orders/` shows one customer's order history to the next visitor. That is a privacy incident, not a bug.

The damage is invisible in your own testing because you are logged in, and every cache skips logged-in users. It shows up as a trickle of support emails, a rising abandoned-checkout rate, and a report finding.

## How WooCommerce signals it

WooCommerce does three things on the cart, checkout and account pages that a well-behaved cache respects:

1. It defines the PHP constant `DONOTCACHEPAGE`. Every major caching plugin checks it and skips the page.
2. It sends `Cache-Control: no-cache, must-revalidate, max-age=0` (WordPress's `nocache_headers()`), plus `Expires` in the past.
3. It sets cookies as soon as a basket has something in it: `woocommerce_items_in_cart=1`, `woocommerce_cart_hash`, and a session cookie named `wp_woocommerce_session_` followed by a hash. A request with those cookies is personal, and caching plugins bypass it.

A caching plugin runs inside WordPress and sees all three. A host cache, a CDN or Varnish runs in front of WordPress and sees only the second and third, in the response headers and the request cookies. Most of them respect `Cache-Control` by default. The failures come from settings that override it: Cloudflare's "Cache everything" with an Edge TTL that ignores origin headers, a host panel's "cache all pages" switch, a Varnish configuration that strips cookies.

## How getReport checks it

> **Free tool:** [WooCommerce store checker](https://getreport.app/tools/woocommerce-checker): The WooCommerce-specific problems in one run: cart fragments slowing every page, a cart or checkout served from a page cache, Product schema without price, availability or rating, out-of-stock products left indexable, plus the plugin cost and duplicate cache plugins from the WordPress Doctor.

Once the page reveals WooCommerce, the report fetches two more addresses, `/cart/` and `/checkout/`, as an anonymous visitor with an empty basket, and reads the response headers. It counts the page as cached when a cache status header contains "hit" (`x-cache`, `cf-cache-status`, `x-litespeed-cache`, `x-nginx-cache`, `x-proxy-cache`, `x-varnish-cache`, `x-kinsta-cache`, `x-sg-cache`, `x-wpe-cache` and a few others), or when the response carries an `age` above 0 together with a public `Cache-Control` that does not say `no-store` or `private`. It does not add anything to a basket and submits nothing.

> **Check: Cart and checkout bypass the page cache.** A cached /cart/ or /checkout/ shows one visitor another visitor's basket, breaks nonces and payment forms, and produces the "session expired" errors shoppers abandon on. Every caching plugin excludes these pages by default; a CDN or host cache in front of them often does not.
>
> 1. Exclude /cart/, /checkout/ and /my-account/ (and their translations) from every cache layer: the caching plugin, the host's page cache and the CDN (Cloudflare: a Cache Rule with "Bypass cache" for those paths and for requests with a woocommerce_* cookie).
> 2. Re-test after purging: the responses should carry cache-control: no-store or a cache status of BYPASS/MISS.

![The Cart and checkout table in the WooCommerce checker: /cart/ and /checkout/ each with their HTTP status, whether the response came from a cache, and the evidence header such as x-cache: HIT or an age value with the cache-control header](https://getreport.app/guides/img/woocommerce-caching-cart-checkout-and-account-pages/cart-cached.webp "The evidence column names the header that proved a cache answered, which also tells you which layer to fix.")

The evidence header is the clue to the layer: `cf-cache-status: HIT` is Cloudflare, `x-litespeed-cache: hit` is LiteSpeed, `x-kinsta-cache`, `x-sg-cache` and `x-wpe-cache` are the named hosts, `x-varnish-cache` or an `age` header with no vendor status usually means Varnish or nginx in front. The addresses are fixed; if your cart lives at `/kosarica/` and `/cart/` answers 404, the finding passes without having seen the real cart, so test the real slug by hand as described under Verify.

Two related findings from the same report:

> **Check: Static files have a cache lifetime.** Without a cache lifetime, every visit downloads the same logo, CSS and scripts again. Returning visitors should get them from their browser in 0 ms.
>
> 1. Send Cache-Control: public, max-age=31536000, immutable for versioned images, CSS, JS and fonts.
> 2. Most caching plugins and CDNs set this for you; check the "browser cache" or "edge TTL" setting.

> **Check: SameSite attribute on cookies.** Without SameSite, the browser attaches the cookie to requests that other sites trigger, which is how cross-site request forgery works. Browsers apply different defaults, so say it explicitly.
>
> 1. Add SameSite=Lax (or Strict) to every cookie.
> 2. Use SameSite=None together with Secure only for cookies a third-party embed genuinely needs.

The first is the opposite goal: static files (images, CSS, scripts) *should* be cached for a long time, and excluding the cart must not switch that off. The second lists cookies set without a `SameSite` attribute. It matters here because the checkout depends on cookies surviving a round trip to a payment provider: `SameSite=Lax` lets the session cookie travel with the top-level redirect back from the gateway, while `Strict` drops it and the shopper lands on an empty cart after paying. Say `Lax` explicitly rather than trusting browser defaults.

## Step by step

### 1. List the real slugs

WooCommerce → Settings → Advanced → Page setup shows which pages are the Cart, Checkout and My account pages. Their slugs are what you exclude. On a translated store each language has its own set (WPML and Polylang register them per language); collect all of them. Add `/checkout/order-received/` and `/checkout/order-pay/` if your exclusions match exact paths rather than prefixes.

### 2. The caching plugin

Most plugins add these exclusions when they detect WooCommerce. Confirm rather than assume:

- **WP Rocket**: automatic. Settings → WP Rocket → Advanced Rules → Never Cache URL(s) shows nothing for them because they are excluded in code; add translated slugs there if they are missing.
- **LiteSpeed Cache**: Cache → Excludes → Do Not Cache URIs (`/cart/`, `/checkout/`, `/my-account/`, one per line, matched as prefixes) and Do Not Cache Cookies (`woocommerce_items_in_cart`, `woocommerce_cart_hash`, `wp_woocommerce_session_`). Keep Cache Logged-in Users off.
- **W3 Total Cache**: Performance → Page Cache → Never cache the following pages: the three paths; Rejected cookies: the three cookie names.
- **WP Super Cache**: Settings → WP Super Cache → Advanced → Rejected URL Strings, plus the cookies under Rejected Cookies.

Also exclude `wc-ajax=` as a query string, where the plugin has such a list.

### 3. The host cache

Managed WordPress hosts (Kinsta, WP Engine, SiteGround, Cloudways and others) run a server-level cache with a WooCommerce preset that already skips the three pages and the cookies. Open the host's caching panel and confirm the preset is on, or add the paths to its exclusion list. On a plain VPS with nginx FastCGI cache, the rules are yours to write:

```nginx
# nginx: inside the server block, before the PHP location
set $skip_cache 0;

# Cart, checkout and account pages, plus AJAX and the Store API
if ($request_uri ~* "/(cart|checkout|my-account)(/|$)") { set $skip_cache 1; }
if ($args ~* "wc-ajax=")                                 { set $skip_cache 1; }
if ($request_uri ~* "^/wp-json/wc/store/")               { set $skip_cache 1; }

# Anyone with a basket, a session or a login
if ($http_cookie ~* "woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_|wordpress_logged_in_") {
    set $skip_cache 1;
}

location ~ \.php$ {
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache     $skip_cache;
    # … fastcgi_pass and the rest of the PHP block
}
```

`fastcgi_cache_bypass` stops serving from the cache; `fastcgi_no_cache` stops storing. Both are needed, otherwise a shopper's cart response gets stored for the next anonymous visitor.

### 4. Varnish

Varnish caches whatever it is told to and strips cookies in many default configurations, which is how it ends up serving carts. In `default.vcl`:

```text
sub vcl_recv {
    # WooCommerce: never cache cart, checkout, account, AJAX or the Store API
    if (req.url ~ "^/(cart|checkout|my-account)(/|$)" ||
        req.url ~ "wc-ajax=" ||
        req.url ~ "^/wp-json/wc/store/") {
        return (pass);
    }
    # Anyone with a basket, a session or a login gets a fresh page
    if (req.http.Cookie ~ "(woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_|wordpress_logged_in_)") {
        return (pass);
    }
}

sub vcl_backend_response {
    # Respect what WooCommerce says
    if (beresp.http.Cache-Control ~ "no-cache|no-store|private") {
        set beresp.uncacheable = true;
        return (deliver);
    }
}
```

`return (pass)` sends the request to the backend and does not store the answer. Put these checks before any rule that removes cookies, or the cookie test never matches.

### 5. The CDN

Cloudflare's default caches static files only; HTML is not cached unless you tell it to with a Cache Rule or APO. APO knows WooCommerce and bypasses the cart cookies. A hand-made "cache everything" rule does not, so add a bypass rule above it: Caching → Cache Rules → Create rule, with the expression

```text
(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.query contains "wc-ajax=")
or (http.request.uri.path contains "/wp-json/wc/store/")
or (http.cookie contains "woocommerce_items_in_cart")
or (http.cookie contains "wp_woocommerce_session_")
or (http.cookie contains "wordpress_logged_in_")
```

and Cache eligibility set to "Bypass cache". Replace or extend the paths with your translated slugs. If your caching rule sets an Edge TTL that ignores origin headers, the bypass rule is the only thing protecting the checkout; without it, WooCommerce's `no-cache` header is overruled.

Other CDNs (Bunny, Fastly, KeyCDN) have the same two knobs under different names: a path or query rule and a cookie rule. Configure both.

### 6. Purge from the inside out

Purge the caching plugin, then the host cache, then the CDN. A stale cart in the host cache would otherwise refill the CDN. Then test.

## Platform notes

### Block checkout

The Cart and Checkout blocks (the default on new stores) render their contents from `/wp-json/wc/store/` in the browser. The page shell is less sensitive than the classic shortcode checkout, but the Store API responses are as personal as the classic page and must bypass the cache; the rules above cover the path.

### Multilingual stores

The translated slugs are the classic gap: `/cart/` is excluded, `/kosarica/` is cached. Every rule above takes a list; put every language's slugs in it. The report only probes `/cart/` and `/checkout/`, so a translated store needs the manual test below.

### Two caching plugins

One excludes the checkout, the other caches it anyway, and each purges only its own copy. Keep one, as in [Two caching plugins: why it breaks and which one to keep](https://getreport.app/guides/two-caching-plugins-why-it-breaks-and-which-to-keep).

## Verify

- Re-run the WooCommerce checker. The finding reads "Cart and checkout bypass the page cache", and the table shows both addresses with `cache-control: no-cache, must-revalidate, max-age=0` and no cache hit.
- From a terminal, twice in a row, with your real slug:

```bash
curl -sI https://example.com/checkout/ | grep -iE "^(cache-control|age|x-cache|cf-cache-status|x-litespeed-cache):"
```

No `age` line, and any status header reads `BYPASS`, `DYNAMIC` or `MISS` both times.

- Two browsers, or one normal and one private window: add a different product in each, open `/cart/` in both. Each shows only its own basket. Check out in one; the other's basket is unchanged.
- After a purchase, `/checkout/order-received/…` in the other browser shows nothing of the first order.
- Static files still cache: the cache-lifetime finding still passes, and a product image request shows `cache-control: public, max-age=31536000` or similar, as explained in [Cache-Control for humans](https://getreport.app/guides/cache-control-for-humans).

## Common mistakes

- **Excluding the paths but not the cookies.** The cart page is safe, but the header mini-cart on every other page is cached with the first shopper's count. Add the cookie rule at the same layer.
- **Bypassing on any cookie.** "Bypass if the request has a cookie" turns caching off for everyone with a consent banner or an analytics cookie. Match the WooCommerce and login cookies by name.
- **Caching `wc-ajax` or the Store API.** The pages are excluded but the requests that fill them are shared. Exclude the query string and the API path.
- **Fixing only the plugin.** The evidence header said `cf-cache-status: HIT`; the plugin was never the problem. Fix the layer the header names.
- **Purging in the wrong order.** CDN first, then host, and the CDN refills from the stale host copy. Inside out, every time.
- **Testing while logged in.** Every cache skips logged-in users, so the checkout looks fine to you. Test in a private window or a second browser.
