HTTP Caching Explained: ETag, Cache-Control and CDNs
Why Your Website Feels Slow (And How Caching Fixes It)
You've optimized your images, minified your CSS, and even upgraded your server. Yet repeat visitors still experience slow load times, and your origin server is burning bandwidth. The culprit? Inefficient HTTP caching. Without proper caching headers, browsers re-download the same assets on every visit, and CDNs can't do their job effectively.
HTTP caching is one of the highest-leverage performance optimizations available. It reduces latency, cuts bandwidth costs, and lightens the load on your origin servers. In this guide, we'll break down the key mechanisms: ETag, Cache-Control, and how CDNs fit into the picture. You'll learn practical strategies to implement them correctly.
How HTTP Caching Works: The Big Picture
When a browser requests a resource, it can either fetch it from the origin server or serve a locally stored copy. HTTP caching defines the rules for when a stored copy is considered fresh and when it must be revalidated.
Two main types of caching exist:
- Browser caching (private): The user's browser stores resources locally. This benefits a single user across multiple pages or visits.
- Shared caching (CDN, proxy): Intermediate servers cache resources for multiple users. This reduces load on your origin and speeds up delivery globally.
Both rely on HTTP headers to decide freshness and revalidation. The two most important headers are Cache-Control and ETag.
Cache-Control: The Freshness Rules
Cache-Control is the primary header for defining caching policies. It's a directive-based header that tells caches how to treat a response.
Key Directives
- max-age: The number of seconds a response is considered fresh. For example,
Cache-Control: max-age=3600means the response is fresh for 1 hour. - s-maxage: Like max-age but specifically for shared caches (CDNs). Overrides max-age for shared caches.
- public: The response can be cached by any cache, including shared ones.
- private: The response is intended for a single user and should not be stored by shared caches.
- no-cache: The response can be stored, but must be revalidated with the origin before each use.
- no-store: The response must not be stored in any cache. Use for sensitive data.
- must-revalidate: Once stale, the cache must not use the response without revalidation.
- immutable: The response will not change during its freshness lifetime. Useful for versioned assets.
Example: Cache-Control: public, max-age=31536000, immutable is ideal for static assets with hashed filenames.
ETag and Conditional Requests
An ETag (Entity Tag) is an identifier for a specific version of a resource. When a resource changes, the ETag changes. Browsers use ETags to make conditional requests: they send the stored ETag in an If-None-Match header. If the resource hasn't changed, the server responds with 304 Not Modified and no body, saving bandwidth.
Similarly, Last-Modified works with If-Modified-Since, but ETags are more precise (they can detect changes within the same second).
How to Generate ETags
Most web servers and frameworks generate ETags automatically. For example, in Express.js you can enable it with app.set('etag', 'strong'). In Nginx, ETags are on by default for static files.
Strong ETags (e.g., "abc123") guarantee byte-for-byte identity. Weak ETags (e.g., W/"abc123") indicate semantic equivalence, not exact bytes.
CDN Caching: Shared Cache at Scale
CDNs (Content Delivery Networks) act as shared caches distributed globally. They cache your content at edge locations, serving it to users from a nearby point of presence. This reduces latency and offloads your origin.
CDNs respect Cache-Control headers but often have their own configuration. Key concepts:
- Edge cache: The CDN's local cache. It stores responses based on cache keys (usually URL + headers like
Accept-Encoding). - Origin shield: An additional caching layer that reduces requests to your origin.
- Cache invalidation: CDNs provide APIs to purge cached content when you update resources.
When using a CDN, set Cache-Control with s-maxage to control shared cache freshness separately from browser cache. For example: Cache-Control: public, max-age=600, s-maxage=3600 means browsers cache for 10 minutes, but the CDN caches for 1 hour.
Comparison of Caching Headers
| Header | Purpose | Example |
|---|---|---|
Cache-Control |
Defines freshness and caching rules | public, max-age=3600 |
ETag |
Unique identifier for a resource version | "abc123" |
Last-Modified |
Timestamp of last modification | Wed, 21 Oct 2025 07:28:00 GMT |
Expires |
Legacy absolute expiration date | Wed, 21 Oct 2025 07:28:00 GMT |
Vary |
Specifies headers that affect caching | Accept-Encoding |
Note: Expires is superseded by Cache-Control but still used by older clients.
Practical Caching Strategy for Web Apps
Follow these steps to implement effective caching:
- Fingerprint static assets: Use hashed filenames (e.g.,
app.a1b2c3.js) and set longmax-agewithimmutable. When the file changes, the hash changes, busting the cache. - Set appropriate Cache-Control for HTML: HTML should usually be
no-cacheor a shortmax-ageso users get updates quickly. UseETagfor revalidation. - Use
Vary: Accept-Encoding: If you serve compressed and uncompressed versions, this ensures caches store them separately. - Leverage CDN with s-maxage: Set longer
s-maxagefor shared caches to reduce origin load, while keeping browser cache shorter if needed. - Invalidate wisely: Use CDN purge APIs when deploying critical updates. For static assets, fingerprinting avoids the need for purging.
- Monitor cache hit ratio: Use CDN analytics to ensure your cache is effective. Low hit ratio means misconfigured headers.
Common Pitfalls and How to Avoid Them
- Over-caching HTML: Users see stale content. Use
no-cacheor shortmax-age. - Under-caching static assets: Set long
max-age(e.g., 1 year) with fingerprinting. - Ignoring
Vary: Caches may serve wrong content (e.g., gzip vs. plain). Always setVary: Accept-Encoding. - Forgetting
privatefor user-specific data: Shared caches might leak data. UseCache-Control: privatefor authenticated responses. - Misusing
no-store: It prevents all caching, which can hurt performance. Use only for sensitive data.
Testing Your Caching Setup
Use browser DevTools (Network tab) to inspect response headers and see if resources are served from cache (look for "(from disk cache)" or "(from memory cache)"). For CDN behavior, use curl -I to check headers like X-Cache or CF-Cache-Status. Tools like WebPageTest can visualize caching across visits.
FAQ
What is the difference between ETag and Last-Modified?
ETag is an opaque identifier that changes when the resource changes, while Last-Modified is a timestamp. ETags are more precise because they can detect changes within the same second and don't rely on clock synchronization.
When should I use no-cache vs no-store?
Use no-cache when you want the cache to store the response but revalidate it with the origin before each use. Use no-store for sensitive data that must never be written to disk or memory by any cache.
How do CDNs handle cache invalidation?
CDNs provide purge APIs that let you remove specific URLs or entire directories from their edge caches. Some also support soft purges that mark content as stale and revalidate on next request. Fingerprinting assets is often more efficient than purging.
Conclusion
Mastering HTTP caching with ETag, Cache-Control, and CDNs is essential for building fast, scalable web applications. Start by setting proper headers for your static assets and HTML, leverage CDN shared caches with s-maxage, and always test your configuration. Small changes in caching headers can lead to significant performance gains.
Need to quickly analyze your server logs to see cache hit ratios? Try our Nginx Log Analyzer to parse and visualize your access logs.