Caching Architecture Across the Stack: Building High-Performance Applications

Caching Architecture Across the Stack: Building High-Performance Applications

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 hour
  • s-maxage=0: Never cache at edge; always revalidate
  • private: 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 OPcache
  • opcache.memory_consumption=256: Memory pool in MB (increase for large codebases)
  • opcache.max_accelerated_files=10000: Number of cached files
  • opcache.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:42 and category: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=86400 allows 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: private for user-specific content (browser only)
  • Use Vary: Cookie to 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:1001 ensures 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

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.

Talk to us →