Development

Multi-Layer Caching for Ecommerce: The Five Layers, What to Cache at Each, and How to Invalidate Without Breaking the Funnel

om ·

Most ecommerce performance problems are caching problems. Either there isn’t enough of it, or there’s too much of it in the wrong places — stale prices in the cart, oversells on the last unit in stock, expired add-to-cart sessions on the checkout page. The teams that get caching right ship fast stores that don’t lie to customers. The teams that don’t ship fast stores that occasionally take orders for products they no longer have.

This piece walks through the five caching layers that matter for an ecommerce site, what to cache at each layer, and — the part most caching guides skip — what to never cache, and how to invalidate without breaking the funnel.

Why ecommerce is a special case

A blog can cache a page for 24 hours without consequence. An ecommerce site cannot. Three categories of data are radioactive:

  • Inventory. Stale stock counts cause oversells, refunds, and angry customers. The damage is permanent and visible.
  • Pricing. Stale prices cause undercharging (lost margin) or overcharging (chargebacks and trust loss).
  • Cart and session state. A single user’s cart has to be invisible to every other user. One mistake here is a data-leak headline.

Everything else — product descriptions, images, category pages, search results — caches well if the layers are configured correctly. The job of a caching strategy is to be aggressive on the safe stuff and surgical with the rest.

The five layers, top to bottom

A request from a customer’s browser to a product page passes through up to five caches. Each one has a job:

1. Browser cache

The user’s browser holds static assets locally. Set Cache-Control: public, max-age=31536000, immutable on hashed asset bundles (app.a8f3.js, main.b91c.css) so they never re-download. Use short TTLs or no-store on HTML responses that vary by user state.

The common mistake here is caching HTML aggressively in the browser. A user who logs out and comes back to a cached "Welcome, Sarah” header has to hard-refresh to fix it. Don’t do that to people.

2. CDN edge cache

The CDN (Cloudflare, Fastly, Cloudfront, Bunny) is where most of the wins live. Cache product images, CSS, JavaScript, and fonts at the edge for long TTLs. Cache HTML for product and category pages where you can — keyed by URL plus the bare minimum of variant data (currency, language).

Crucially: the CDN should never cache logged-in HTML. Use a cookie-presence rule ("bypass cache if wordpress_logged_in_* or woocommerce_session is set”) to route authenticated requests directly to the origin. Cloudflare’s Cache Rules and Fastly’s VCL both handle this; check your config explicitly.

3. Reverse proxy / page cache (Varnish, Nginx fastcgi_cache)

The page cache lives in front of your origin application. For WordPress and WooCommerce, this is typically Nginx fastcgi_cache or, for higher-traffic sites, Varnish with VCL rules. The page cache holds rendered HTML for guests so PHP never executes for anonymous traffic.

The bypass rules at this layer are the same as the CDN: any presence of cart, login, or checkout cookies must skip the cache. The bypass list for WooCommerce in particular needs to include woocommerce_items_in_cart, woocommerce_cart_hash, and the session cookies. Get this wrong and a user sees another user’s cart.

4. Object cache (Redis or Memcached)

The object cache holds the result of database queries, transients, and expensive computations. For WooCommerce, this is the difference between "product page renders in 200ms” and "product page renders in 2 seconds.”

Persistent object caching via the official Redis Object Cache plugin (or your hosting provider’s equivalent) is non-negotiable above ~50 SKU. WooCommerce’s database churn pattern — hammering wp_options for transients, wp_postmeta for product fields — is exactly the workload Redis was designed for.

5. Database query cache and OPCache

OPCache is the PHP opcode cache that stops the interpreter from re-parsing your code on every request. Enable it; tune opcache.memory_consumption and opcache.max_accelerated_files based on your codebase size. WooCommerce + a typical plugin stack needs at least 256 MB and 20,000 files to avoid eviction churn.

The database itself: query cache is largely deprecated in MySQL 8 (it was removed). Indexing is what matters. Run EXPLAIN on your slowest queries and add indexes where the query plans show full table scans. WooCommerce’s HPOS migration helped here significantly — if you haven’t moved orders to HPOS yet, that’s the highest-leverage indexing work you can do.

What to never cache

The hard list, regardless of layer:

  • /cart, /checkout, /my-account, and any AJAX endpoint that touches them.
  • Any page rendered for a logged-in user (bypass via cookie rule).
  • Search results that depend on per-user filters or geographic state.
  • Inventory counts displayed inline. Render them as a separate AJAX call that pulls fresh data, with a 30–60 second TTL at most.
  • Pricing for users in different tax zones if you display prices including tax. The page itself can cache; the price block needs to be either edge-personalized by country or fetched live.

The discipline here is to be paranoid about freshness on anything that affects a financial decision and aggressive everywhere else.

Cache invalidation: the part that breaks

The classic Phil Karlton line is true: cache invalidation is one of the two hard problems in computer science. In ecommerce, the most common failures look like:

Stock changes that don’t propagate. An admin updates inventory in WP admin; the product page still shows the old count for an hour. Fix: hook into woocommerce_product_set_stock and explicitly purge that product’s URL from the page cache and CDN.

Price changes during a sale. A flash sale starts at 9 AM; cached pages show the old price for 20 minutes. Fix: pre-purge product URLs at the moment the sale begins, ideally via a scheduled task that fires the purge a few seconds before the sale activates.

Category pages that go stale. A product is published; it doesn’t appear in its category for hours. Fix: hook into save_post_product and purge the parent category URLs in addition to the product URL.

Search index drift. Algolia, Meilisearch, and Elasticsearch all need explicit re-indexing on product changes. The Stock-Saved hook is a good trigger; running a nightly full re-index is a defensive net under that.

The pattern: every layer needs to know how to be told something changed. Build a single "product modified" event that fans out to (1) page cache purge, (2) CDN purge for that URL and its parents, (3) search index update, and (4) Redis transient flush for related keys. Wire that to WooCommerce’s existing hooks. Test it.

The numbers we aim for

For a properly tuned WooCommerce store, the targets we hold to in production are:

  • TTFB: under 200ms for cached guest requests, under 600ms for authenticated requests.
  • LCP: under 2.0s on mobile (the Core Web Vitals threshold for "good” is 2.5s).
  • Cache hit ratio at the CDN: above 85% for total requests, above 95% for static assets.
  • Object cache hit ratio: above 95% (most well-tuned Redis instances run 99%+).

If you’re missing one of these by a wide margin, the bottleneck is usually clear: low CDN hit rate means a cookie rule is too aggressive (everything is bypassing); low object cache hit rate means transients are being evicted (Redis maxmemory too low or a plugin is creating uncached duplicates).

What’s worth your time first

If you’re auditing an existing store and trying to figure out where to start, this is the order:

  1. Confirm Redis object cache is on and persistent. Every other layer is multiplied by this one.
  2. Audit your CDN cache rules. Specifically: are you caching anonymous HTML at the edge? Most stores aren’t, and could be.
  3. Move WooCommerce orders to HPOS. If you haven’t, this is the single biggest database performance win available.
  4. Tune OPCache. Memory and file count limits eat performance silently.
  5. Set up explicit cache purge on stock and price changes. Stale-by-default is worse than uncached-by-default.

Most of these are configuration rather than code. The performance wins are real and they don’t require rewriting your application.

If you’d like an audit of where your store sits against these benchmarks, our engineering team can run through the layers and tell you which ones are actually slowing you down. Most of the wins are usually in two layers, not all five.

FAQ

What’s the single most impactful caching change for a typical WooCommerce store?

Enabling persistent Redis object caching, if it isn’t already on. The performance delta is usually 3–5x on backend response time. Every other layer compounds on top of this one.

Can I cache logged-in users?

Generally no. Per-user state — cart contents, account links, prices in user-specific currencies — makes full-page caching for authenticated requests dangerous. The pattern that works is to cache the static skeleton and load user-specific blocks via authenticated AJAX requests that hit the origin.

How long should I cache product pages at the CDN?

For guest traffic on a stable catalog, an hour is reasonable with explicit purge on product changes. The TTL itself matters less than whether your purge mechanism is wired up correctly. A 24-hour TTL with reliable purge beats a 5-minute TTL without.

Why does my cart sometimes show another user’s items?

The page cache is serving cart pages without bypassing on session cookies. Check your Nginx fastcgi_cache_bypass rules and your CDN cache rules — both need to skip caching when woocommerce_session_*, woocommerce_cart_hash, or wp_woocommerce_session_* cookies are present.

Is Varnish necessary in 2026?

For WooCommerce specifically, Nginx fastcgi_cache handles most use cases at lower operational complexity. Varnish wins for stores with custom routing, complex VCL needs, or stores serving 100k+ requests per minute where its in-memory architecture pays off. Below that, Nginx is usually the right call.

Written by Mradul, EtherLabz engineering. If your store is missing the Core Web Vitals targets and you’re not sure which layer is to blame, get in touch and we’ll take a look.