Architecture
Designing a CDN and Cache Strategy for Dynamic Websites
A practical model for cache keys, freshness, invalidation, private data, and observability across browsers, CDNs, reverse proxies, and applications.
Author: Fahad Bin Shakir · Published: · Updated: · 12 min read
Introduction
Caching improves latency and reduces origin load, but an unsafe cache key can leak private content and an unclear invalidation plan can keep incorrect pages online. The design problem is not whether to enable a CDN. It is deciding which representation may be reused, for whom, for how long, and how every layer will learn that the representation changed.
A useful strategy begins with content classes and explicit ownership. It avoids one global rule for HTML, assets, APIs, and authenticated pages, because those surfaces have different privacy, correctness, and freshness requirements.
Map the cache layers and content classes
Draw the request path from browser to CDN, load balancer, reverse proxy, application, and data store. At each layer record the cache key, TTL, stale behavior, purge mechanism, and diagnostic headers. A response can be fresh in one cache and stale in another, so troubleshooting requires visibility across the chain.
Classify responses as immutable versioned assets, public documents, frequently changing public data, personalized content, authenticated data, mutations, and errors. This classification should drive headers and tests. Hashed JavaScript can be cached for a year; account HTML normally should not be stored by a shared cache at all.
Build a complete cache key
The key must include every request property that changes the representation. The normalized path and relevant query parameters are common inputs, but language, device variants, compression, tenant, currency, or an experiment may also matter. Avoid including tracking parameters that create duplicate objects, and never ignore an authentication or authorization dimension when a shared cache can see the response.
Use Vary sparingly because unrestricted header variation fragments the cache. Normalize at the edge where possible and test that two requests expected to differ never receive the same stored object. Also test the inverse: harmless marketing parameters should not bypass caching or create unbounded storage growth.
Express freshness intentionally
Cache-Control should describe the response rather than the platform. Use public with a shared-cache lifetime for reusable public documents, private for browser-only reuse, and no-store for sensitive responses that must not be retained. no-cache permits storage but requires revalidation, which is different from forbidding storage.
Stale-while-revalidate can protect latency while a public object refreshes, and stale-if-error can improve resilience during a short origin failure. Both trade freshness for availability, so define a bounded window and exclude responses where stale data creates security, legal, or transactional risk.
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30
ETag: "content-version-42"Choose invalidation before launch
Time-based expiry is simple but insufficient for urgent corrections. Prefer versioned asset URLs so a deployment never needs to purge immutable files. For HTML and API data, define purge keys or tags that connect a changed record to every derived representation.
Make invalidation idempotent, authenticated, observable, and safe to retry. When a purge fails, the publishing system should report partial completion instead of announcing success. For large changes, deploy new objects first, switch references second, and retire old versions after the maximum cache lifetime.
Prevent cache-related data exposure
Never rely on obscurity or a cookie name alone to protect personalized responses. Set private or no-store at the application, prevent edge overrides, and verify the final delivered headers. Strip untrusted forwarding headers before they influence cache keys or canonical URLs.
Test with two independent sessions: request private content as one identity, then repeat the same URL as another identity and anonymously. Include error pages, redirects, range responses, and API failures. A cached 302 containing a token or a cached 500 with diagnostic detail can be as harmful as cached account HTML.
Operate caches with evidence
Expose safe diagnostics such as Age, cache status, object version, and request ID. Monitor hit ratio by content class, origin latency, revalidation volume, purge failures, and stale responses served during errors. A rising global hit ratio is not useful if the most expensive route still misses.
Document an emergency bypass that can disable caching for a bounded route without turning off the entire CDN. Exercise it before an incident. The goal is a system whose cached behavior can be explained from a single response and changed without guesswork.
Deployment checklist
- Classify every route as immutable, public, private, mutating, or error content.
- Document each layer's cache key, TTL, purge, and diagnostic headers.
- Normalize only parameters that cannot change the representation.
- Keep personalized and sensitive responses out of shared caches.
- Use versioned URLs for immutable deployment assets.
- Test invalidation, session isolation, stale behavior, and origin failure.
- Monitor by content class rather than one site-wide hit ratio.
