Caching is the most powerful performance multiplier in modern software. Yet most teams treat it as an afterthought—bolting on layers without understanding how they interact, leaving performance on the table and introducing subtle bugs like stale data and cache inconsistency.
A single cache layer cannot solve every problem. Browser caches, CDN edges, object stores, and opcode caches all exist because different parts of the request lifecycle have different bottlenecks. Understanding how these layers compose and when to invalidate each one separates systems that deliver sub-100ms responses from those that frustrate users.
This guide covers the full-stack caching architecture that powers e-commerce fleets at scale: how to layer caches, coordinate invalidation across layers, and avoid the pitfalls that catch most teams.
The Five Caching Layers
A robust caching architecture spans from the user’s browser all the way to the application server’s memory. Each layer guards a different bottleneck and serves a different audience.
1. Browser Cache: The Client’s Memory
When a user’s browser loads a web page, it stores static resources—images, stylesheets, scripts, fonts—locally on disk or in memory. The browser uses HTTP headers like Cache-Control, Expires, and ETag to determine freshness.
According to web.dev’s official HTTP caching guide, the Cache-Control header is “the primary mechanism for managing caching behavior” across all cache layers. When a user revisits the same page, the browser checks its local cache first. If the resource is still fresh, the browser makes zero network requests. This alone eliminates most bandwidth usage and drastically improves perceived performance.
Best practice: Use versioned (fingerprinted) URLs for immutable assets. As web.dev advises, “When responding to requests for URLs that contain fingerprint or versioning information, and whose contents are never meant to change, add Cache-Control: max-age=31536000.” Build tools like webpack automate this by embedding content hashes in filenames (e.g., style.x234dff.css). Content updates automatically trigger new URLs, forcing fresh downloads without cache pollution.
2. CDN / Edge Cache: Content at the Edge
CDN servers (Cloudflare, Akamai, AWS CloudFront) sit between users and your origin server. When a visitor in London requests content originally served from New York, a CDN serves it from a London edge node if cached, dramatically reducing round-trip latency from 100ms+ to <50ms.
CDNs cache both static and dynamic content, though behavior varies. As Jono Alderson’s HTTP caching guide notes, “Cloudflare by default doesn’t cache HTML—requiring explicit configuration.” This is intentional: HTML changes frequently and often contains personalized data, so edge caching HTML recklessly creates stale-data nightmares.
Control edge caching with the Cache-Control header’s s-maxage directive, which overrides max-age specifically for shared caches like CDNs:
s-maxage=3600: Cache at edge for 1 hours-maxage=0: Never cache at edge; always revalidateprivate: Never cache at edge; browser only
For WooCommerce and e-commerce, edge caching requires surgical precision. WooCommerce-Cloudflare best practices explicitly warn: exclude /cart, /checkout, and /my-account from edge caching. Caching these pages causes customers to see outdated cart contents and leaks personalized data. Use Cloudflare Page Rules to bypass caching on these URLs.
3. Full-Page Cache: Templating at the Server Edge
Full-page caching (e.g., WP Rocket, LiteSpeed Cache, Varnish) stores rendered HTML at the server level, serving the exact same HTML to every visitor until TTL expires. This works for static landing pages and public content but fails for personalized or dynamic pages.
Key constraint: Never cache pages with user-specific data, session tokens, or form submissions. A full-page cache that accidentally serves one user’s account page to another is a security breach and data leak.
Legitimate uses: Product listing pages, blog posts, marketing pages, category pages, FAQs. Most e-commerce sites cache these for 1–24 hours, depending on update frequency.
4. Redis / Object Cache: Database Query Results in RAM
The database is often the slowest link. Redis (or Memcached) stores expensive computation results, frequently queried data structures, and ORM results in RAM, eliminating repeated database hits.
Redis stores all data in memory, achieving sub-millisecond latency (microseconds) versus millisecond-level database queries. Kinsta’s Redis for WordPress guide documents real-world benchmarks: “a fresh WordPress installation showed almost 50% reduction in page load times” with Redis object caching enabled.
When Redis is most effective:
- High-traffic spikes: Redis buffers surge load, serving cached data while the database breathes
- Heavy queries: Caching results of expensive joins or aggregations (top 10 products, category sums)
- Repeated reads: Session data, user preferences, post counts, term meta
Redis is not a silver bullet. WebHostMost’s Redis reality check warns: “If your wp_options autoload data is over 2MB, Redis is caching bloat.” Audit your database first. Remove dead data from wp_options, wp_postmeta, and Action Scheduler tables before adding Redis. A cache hit ratio below 80% signals Redis isn’t helping.
5. OPcache: PHP Opcode Cache in the Engine
OPcache (Zend OPcache, included in PHP 5.5+) compiles PHP source code to bytecode once and caches the bytecode in shared memory. Without OPcache, PHP recompiles every file on every request. With OPcache enabled, compilation happens once per code deployment.
Performance gain: Up to 70% CPU reduction on busy sites. This is the “easiest win” and most hosts enable it by default. Verify with phpinfo() or php -m | grep opcache.
Configuration:
opcache.enable=1: Enable OPcacheopcache.memory_consumption=256: Memory pool in MB (increase for large codebases)opcache.max_accelerated_files=10000: Number of cached filesopcache.validate_timestamps=0(production): Skip file-change detection, reload via deployment
Vilee LLC combines deep technical expertise in WordPress/WooCommerce development with AI-powered automation to operate 520+ profitable online businesses at scale.
How the Layers Compose: The Request Flow
Understanding the order in which caches are checked is critical to building correct behavior:
| Stage | System | Latency | Hit = Do This |
|---|---|---|---|
| Browser | Local disk / memory | <5ms | Use cached file, zero network request |
| Network | User’s ISP cache (optional) | 10–50ms | May use cache; depends on ISP setup |
| CDN Edge | Cloudflare / AWS / Akamai | 20–100ms | Return cached response from edge |
| Full-Page Cache | Varnish / WP Rocket / LiteSpeed | 5–20ms | Return rendered HTML without PHP |
| Object Cache | Redis / Memcached | <1ms | Return pre-computed data without DB query |
| OPcache | PHP shared memory | Microseconds | Return compiled bytecode without parsing |
| Database | MySQL / PostgreSQL | 5–50ms | Execute query (slowest path) |
A well-designed request usually hits three caches: browser → edge → Redis → no database query. A cache miss at every layer falls through to the database, the costliest path.
Cache Invalidation: The Hard Problem
Cache invalidation is notoriously difficult. The joke in engineering is: “There are only two hard problems in Computer Science: cache invalidation and naming things.” This is not entirely wrong.
The problem: When data changes, all caches holding that data become stale. You must invalidate layers in the correct order, or you create inconsistency windows where different users see different truths.
Invalidation Strategies
Codelit’s cache invalidation strategies guide outlines three primary approaches:
1. Time-to-Live (TTL) Based
Set an expiration time on each cache entry. When TTL expires, the cache automatically discards it. Simplest but least precise:
- Pros: No coordination required; automatic cleanup
- Cons: Stale data served for up to TTL duration; over-caching wastes resources
Good for: Product prices, inventory counts, social media feeds (where slight staleness is acceptable)
2. Event-Driven Invalidation
When data changes (product updated, order placed, comment posted), immediately purge affected cache entries. Requires coordination between application and cache layers.
Example: When product #42 is updated:
- Update database
- Purge
product:42:*from Redis (pattern matching) - Call CDN API to purge
/product/42/from edge - Optionally purge full-page cache for affected categories
Pros: Data is fresh immediately after changes
Cons: Complex orchestration; must handle failure cases (e.g., CDN API timeout)
3. Tag-Based Invalidation
Group cache entries by semantic tags. When data changes, invalidate all entries with a matching tag. More granular than URL-based purging:
- Tag cached responses:
product:42, category:electronics, sale:summer2026 - Update product: Purge tags
product:42andcategory:electronics - Start summer sale: Purge tag
sale:summer2026(invalidates all sale banners at once)
Cloudflare’s Cache Rules for WordPress/WooCommerce support tag-based purging via API, allowing precise invalidation without wasting purge quota.
Purge Ordering: The Thundering Herd Problem
When a popular cache entry expires, multiple requests may hit the database simultaneously. This “thundering herd” creates a spike and can crash the database or origin server.
Solution: Use cache stampede protection:
- Stale-While-Revalidate: Serve stale cache while fetching fresh data in background. Web.dev documents:
Cache-Control: stale-while-revalidate=86400allows serving cached content up to 1 day after expiry while revalidating in the background. - Distributed Lock: Only one worker refreshes cache; others wait or use stale value
- Staggered TTLs: Don’t expire all entries at once; vary TTLs by 10–20% so refreshes spread over time
TTL Strategy and Cross-Layer Coordination
Different caches need different TTLs based on data volatility and consistency requirements:
| Content Type | Browser Cache | CDN TTL | Redis TTL | Reason |
|---|---|---|---|---|
| Static JS/CSS | 1 year | 1 year | N/A | Versioned URLs; immutable |
| Product HTML | 1 hour | 6 hours | 24 hours | Product metadata changes; edge slower to purge |
| Category listing | 30 min | 4 hours | 12 hours | Inventory volatile; users expect fresh stock |
| User session | N/A | No cache | 30 min | Personal; never edge cache |
| Cart data | N/A | No cache | Expires at session end | Real-time updates; must not stale |
Key rule: Edge cache TTL should always be >= browser cache TTL. If browser TTL is longer than edge TTL, browser serves stale content while edge has fresh—creating confusion and bugs.
Avoiding Stale and Personalized Data Leaks
Caching introduces two critical security and correctness risks:
Stale Data
A cached response older than the underlying data. Examples:
- Price changes but cache not invalidated → customer sees old price
- Inventory depleted but full-page cache not purged → overselling
- Comment deleted but edge cache persists → showing ghosted replies
Prevention: Design invalidation to fire before or immediately after data changes. Use event-driven invalidation for critical data; TTL-only for non-critical.
Personalized Data Leaks
Cached content containing one user’s personal data served to another. Examples:
- User account page cached and returned to unauthenticated visitor
- Cart contents cached → shown to different user
- Dashboard with order history cached → visible to attacker
Prevention:
- Never cache authenticated responses at edge or full-page level
- Use
Cache-Control: privatefor user-specific content (browser only) - Use
Vary: Cookieto split cache by session (be careful: creates many cache entries) - Explicitly bypass caching for
/my-account,/checkout,/dashboard, etc.
As WooCommerce Cloudflare best practices state: “Proper configuration allows stores to harness the benefits of Cloudflare without disrupting the dynamic functionality that WooCommerce relies on.”
Designing for E-Commerce Fleets at Scale
Operating hundreds or thousands of e-commerce sites requires caching to be both autonomous and coordinated.
Multi-Tenant Isolation
Each site’s cache must not pollute others. Strategies:
- Separate Redis databases: Each tenant uses its own Redis DB (0–15)
- Key prefixing:
tenant:42:product:1001ensures no collisions - Shared Redis Cluster with namespacing: One cluster serves all; apps prefix keys
Automated Invalidation
Manual purging doesn’t scale. Implement:
- Database triggers → cache invalidation queue
- Message bus (Redis Streams, Kafka) → workers that purge CDN/full-page caches
- API hooks → React to product updates, order placement, etc.
Monitoring and Metrics
Blind caching is dangerous. Track:
- Cache hit ratios: >80% is healthy
- Stale-data incidents: How often cache served outdated content?
- Miss spikes: Sudden jump in misses signals invalidation or server crash
- Memory usage: Redis eviction rate (should be <5%)
Caching Checklist for E-Commerce Teams
- Enable OPcache in PHP; verify
php -m | grep opcache - Configure browser cache with versioned URLs for static assets (
max-age=31536000) - Set up CDN (Cloudflare or similar); enable HTML caching only for public pages
- Exclude dynamic pages from edge cache:
/cart,/checkout,/my-account,/dashboard - Deploy Redis object cache; monitor hit ratio (>80%)
- Audit database for dead data before caching; clean
wp_options,wp_postmeta - Implement event-driven invalidation for critical data (prices, inventory)
- Use tag-based purging at CDN to avoid wasting quota
- Set stale-while-revalidate for resilience:
Cache-Control: stale-while-revalidate=86400 - Test cache behavior with real data; simulate user journeys (add to cart, checkout, account access)
- Monitor cache hit ratio, eviction rate, stale-data incidents; alert on anomalies
- Document cache invalidation flow for your team; make it a runbook
FAQ
Q: Should we cache with Redis or full-page cache?
A: Both. Redis caches database queries and expensive computations. Full-page cache (WP Rocket, LiteSpeed) caches rendered HTML for public pages. They serve different purposes and work together. Full-page cache is faster (skip PHP) but less flexible (no personalization). Use full-page for static landing pages; Redis for personalized content like product recommendations.
Q: What if a cache layer fails (Redis down, CDN unreachable)?
A: Design for graceful degradation. If Redis is down, fall back to database queries (slower but correct). If CDN is unreachable, serve from origin (slow but available). Use health checks and circuit breakers. For resilience, set Cache-Control: stale-if-error=604800 to serve stale cache if origin fails.
Q: How do we avoid the thundering herd when a popular cache entry expires?
A: Use stale-while-revalidate (serve stale while refreshing in background), distributed locks (only one worker refreshes), or staggered TTLs (vary by 10–20%). Redis optimization strategies recommend proactive cache refresh: periodically refresh entries before they expire rather than waiting for expiry.
Q: Is there a universal TTL that works for all content?
A: No. Static assets get long TTLs (year+) because URLs are versioned. Product data gets medium TTLs (hours) based on how often it changes. Personal data (cart, account) gets short TTLs or no caching. Design TTLs per content type based on update frequency and consistency requirements.
Conclusion
Caching is a force multiplier for performance and cost. A well-designed multi-layer cache architecture can reduce database load by 90%, cut latency to <50ms globally, and handle traffic spikes that would otherwise crash the origin server.
The key is understanding each layer’s purpose, coordinating invalidation, and designing for failure. Build caching as a first-class citizen in your architecture—measure hit ratios, automate purging, and test invalidation flows with real data. Teams that master caching win on performance, cost, and reliability.
For e-commerce teams operating at scale, caching is non-negotiable. Start with browser cache and versioned URLs (easiest win), add CDN edge caching for public content, layer Redis for hot data, and implement event-driven invalidation for correctness. Monitor continuously. Get expert help or use our contact form. The investment pays off in milliseconds and millions of satisfied customers.
Sources
- web.dev – Prevent Unnecessary Network Requests with HTTP Cache
- Jono Alderson – A Complete Guide to HTTP Caching
- Kinsta – Redis Object Caching for WordPress
- Redis – Guide to Cache Optimization Strategies
- WooCommerce & Cloudflare – Caching Best Practices for Optimal Performance
- Codelit – Cache Invalidation Strategies
- Cloudflare – Cache Rules for WordPress/WooCommerce
- WebHostMost – Redis Reality Check: Why Just Enabling Redis WordPress Does Nothing
Frequently Asked Questions
What is the difference between browser cache and CDN cache?
Browser cache stores resources locally on the user’s device (sub-5ms access). CDN cache stores content on edge servers globally (20–100ms from user). Browser cache only benefits that single user; CDN cache is shared across all users in a region. Use both: browser cache for static assets with long TTL, CDN for public content with moderate TTL, no CDN for personalized content.
How do we know if Redis object cache is actually helping?
Monitor cache hit ratio. A healthy ratio is >80%. If below 80%, Redis may not be helping—measure before enabling. Also check eviction rate (% of entries Redis discards when memory fills). If eviction is high, increase Redis memory or reduce cached data size. Finally, benchmark page load time before and after Redis with real traffic data.
Should we cache HTML pages even though content changes frequently?
Only cache HTML for public, non-dynamic pages (landing pages, blog posts, product listings, FAQs). Never cache authenticated pages (/my-account, /checkout, /dashboard) because cached data leaks to other users. For frequently-updated content like inventory counts, use short TTLs (1–4 hours) with event-driven invalidation, or skip full-page cache and rely on Redis for hot data.
