Infrastructure & Networking
What is HTTP caching?
HTTP caching is the mechanism by which web browsers, CDN edge servers, reverse proxies, and other intermediaries store copies of HTTP responses, reusing stored responses for subsequent requests rather than fetching fresh content from the origin server every time. By serving cached responses caching reduces latency for users, who receive responses from nearby or local caches rather than from distant origin servers, reduces bandwidth consumption, the same content is not repeatedly transmitted from origin to user, and reduces load on origin servers, which handle only requests that cannot be served from cache.
HTTP caching is controlled through HTTP headers, the Cache-Control header is the primary mechanism, supplemented by Expires, ETag, Last-Modified, Vary, and other headers. These headers allow origin servers to specify exactly how responses should be cached, how long they remain fresh, whether they can be shared across users, and how clients should validate whether cached content is still current before using it.
Caching is pervasive in web infrastructure, occurring simultaneously at multiple layers. The browser maintains a local cache. CDN edge servers maintain distributed caches. Reverse proxies maintain caches in front of application servers. Each caching layer reduces the load on layers further upstream, a request served from the browser cache never touches the network. A request served from a CDN edge cache never reaches the origin server. Understanding how caching works at each layer and how cache control headers govern behaviour across all layers is essential for web performance optimisation and correct redirect implementation.
Cache-Control header
The Cache-Control header is the primary HTTP caching control mechanism, a response header that specifies how the response should be cached, for how long, and under what conditions.
max-age: the most commonly used Cache-Control directive. Specifies the maximum time in seconds that a response is considered fresh, valid for serving from cache without revalidation. Cache-Control: max-age=86400 instructs caches to store the response and serve it without revalidation for 86,400 seconds, 24 hours. After the max-age expires the cached response is stale, the cache must revalidate with the origin before serving it.
s-maxage: like max-age but applies specifically to shared caches, CDN edge servers and reverse proxies rather than private browser caches. Cache-Control: max-age=3600, s-maxage=86400 tells browsers to cache for 1 hour while CDN edge servers cache for 24 hours, enabling different caching durations for different cache layers.
no-cache: does not mean the response cannot be cached, it means the cached response must be revalidated with the origin before serving. A response cached with no-cache is stored but every subsequent request triggers a conditional request to the origin, checking whether the cached version is still current. If the origin confirms the cached version is current, returning a 304 Not Modified response, the cache serves the stored response without retransmitting the response body. If the origin returns a new response the cache updates its stored version.
no-store: genuinely prevents caching. A response with Cache-Control: no-store must not be stored in any cache, private or shared. Every request for the resource goes to the origin and receives a fresh response. Appropriate for responses containing sensitive personal data, bank account information, healthcare records, that should never be retained in any cache.
public: explicitly marks the response as cacheable by shared caches, CDN edge servers and proxies, even when the response was received over an authenticated connection. Responses to authenticated requests are not cached by shared caches by default, the public directive overrides this default when caching of authenticated responses is intentional.
private: marks the response as cacheable only in private caches, the user’s browser, not by shared caches. Cache-Control: private, max-age=3600 allows browser caching for 1 hour but prevents CDN and proxy caching. Appropriate for personalised responses that are specific to a user, a page that shows the user’s name and account details should be browser-cached but not CDN-cached.
must-revalidate: once a cached response becomes stale, after max-age expires, must-revalidate requires the cache to revalidate with the origin before serving the stale response. Without must-revalidate some caches may serve stale responses when the origin is unreachable, with must-revalidate the cache must return an error rather than serve stale content when revalidation is not possible.
immutable: indicates that the response body will not change during its freshness lifetime, even if the user manually refreshes the page. Used for versioned static assets, CSS and JavaScript files with content-hash filenames. Cache-Control: max-age=31536000, immutable caches the versioned asset for one year without revalidation even on manual refresh, because the URL will change if the content changes. Immutable significantly improves performance for return visits by preventing unnecessary revalidation requests.
Validation headers
Validation headers enable conditional requests, allowing caches to check whether stored responses are still current without re-downloading the full response body.
ETag: an Entity Tag, a unique identifier for a specific version of a response. The origin server generates an ETag for each response, typically a hash of the response content or a version identifier. ETag: "abc123def456" is an ETag in the response header. When a cached response expires the cache sends a conditional GET request including If-None-Match: "abc123def456": asking the origin whether the content has changed since the ETag was generated. If the content is unchanged the origin returns 304 Not Modified with no response body, the cache serves the stored response and updates its cache record. If the content has changed the origin returns a 200 OK response with the new content and a new ETag, the cache stores the new response.
ETag-based validation is the preferred validation mechanism, ETags are more precise than date-based validation and work correctly even when content is regenerated with identical content at different times.
Last-Modified and If-Modified-Since: date-based validation. The origin server includes a Last-Modified header indicating when the resource was last changed. Last-Modified: Wed, 15 Jan 2025 10:00:00 GMT. When a cached response expires the cache sends If-Modified-Since: Wed, 15 Jan 2025 10:00:00 GMT: asking whether the content has changed since that date. The origin returns 304 Not Modified if unchanged or 200 OK with new content if changed.
Date-based validation is less reliable than ETag-based validation, two versions of content generated at different times may be identical, and sub-second content changes are not detectable with second-precision timestamps. Modern implementations use ETags preferentially.
Vary header
The Vary header tells caches that the response varies depending on specific request headers, caches must store separate cached versions for different values of the specified headers.
Vary: Accept-Encoding: the most common Vary header. Indicates that the response varies based on the Accept-Encoding request header, the client’s compression support. A client that supports gzip receives a gzip-compressed response. A client that does not support gzip receives an uncompressed response. Caches must store separate versions for gzip-capable and non-gzip clients, serving the appropriate version to each.
Vary: Accept-Language: indicates that the response varies based on the client’s language preference. Different language versions of the same URL are cached separately, ensuring French-speaking users receive the French version and English-speaking users receive the English version.
Vary: *: indicates that the response varies based on all request headers, effectively preventing shared caching. Every request is treated as unique and must be served from the origin.
Overuse of Vary headers reduces cache efficiency, each unique combination of varied header values requires a separate cache entry for the same URL. Cache key explosion, too many cache entries for the same URL, can overwhelm cache storage.
Browser cache vs CDN cache
HTTP caching operates at multiple independent layers, browser caches and CDN caches behave differently and respond to the same headers in different ways.
Browser cache: the user’s local browser maintains a private cache, storing responses for the specific user only. Browser-cached responses are served without any network request, the fastest possible response time. Browser caches respect private, max-age, no-cache, no-store, and other Cache-Control directives. The browser cache persists across browser sessions, a page cached in one session is available from cache in subsequent sessions until it expires.
Browser caching provides the best user experience for return visitors, pages they have recently visited load from the local cache without network requests. However browser caches are per-user and per-device, a user’s browser cache on their laptop is separate from their browser cache on their phone. Browser caches cannot be shared across users.
CDN cache: shared caches at CDN edge servers, accessible to all users connecting to that edge server. A resource cached at a CDN edge server is shared across all users who request it from that edge location, the first user’s request populates the edge cache and all subsequent users benefit from the cached response. CDN caches respect public, s-maxage, max-age, no-cache, no-store, and Cache-Control directives.
CDN caches provide the greatest performance benefit for content accessed by many users, the first request to an edge server fetches from the origin but all subsequent requests are served from the edge. CDN cache hit rates measure the proportion of requests served from edge cache, higher hit rates mean less origin load and faster responses.
Cache layering: browser and CDN caches operate independently but complementarily. A request from a user’s browser for a frequently accessed resource might be served from the browser cache, never touching the CDN. A request for a resource not in the browser cache goes to the CDN edge, which serves from its cache without reaching the origin. Only a CDN cache miss, a resource not in any cache or an expired cache entry, results in an origin request. The layers together minimise origin load and maximise response speed.
HTTP caching and redirects
HTTP caching interacts with redirects in important ways, redirect responses can be cached, and cached redirects create specific management challenges.
301 permanent redirect caching: browsers cache 301 permanent redirects, a browser that follows a 301 from http://example.com/page to https://example.com/page stores this redirect in its cache. Subsequent requests for http://example.com/page are served from the browser cache, the browser goes directly to https://example.com/page without contacting the server. This caching improves performance, cached redirect following is faster than round-tripping to the server for a redirect response.
However cached 301 redirects create a management challenge, if the redirect destination changes the browser’s cached redirect continues to send users to the old destination until the cache expires. 301 redirects are cached with the browser’s default cache duration, effectively permanent unless explicitly controlled with Cache-Control headers. A Cache-Control: max-age=3600 header on a 301 response limits browser caching to 1 hour, making destination changes propagate more quickly.
302 temporary redirect caching: browsers do not cache 302 temporary redirects by default, every request for the redirected URL goes through the redirect processing. This non-caching behaviour is why URL shorteners and analytics-tracked redirects use 302 rather than 301, every click is tracked because every click triggers a redirect server request rather than being served from browser cache.
CDN caching of redirect responses: CDN edge servers cache redirect responses according to their Cache-Control headers, just as they cache other responses. A 301 redirect served from a CDN edge server with Cache-Control: max-age=86400 is cached at the edge for 24 hours. All users connecting to that edge server receive the cached redirect response without the redirect being processed at the origin. CDN cache invalidation, explicitly purging cached redirect responses, is necessary when redirect configurations change and immediate propagation is required.
Stale cached redirects: the most common redirect caching problem. A redirect destination is changed but browsers or CDN edge servers have cached the old redirect. Users continue to be sent to the old destination until caches expire. Several approaches mitigate this problem:
Short Cache-Control max-age on redirect responses, max-age=3600 rather than the effectively-permanent default for 301s, ensures caches expire quickly when redirects change.
Explicit cache invalidation at the CDN, purging cached redirect entries immediately when redirect configurations change.
Monitoring for stale redirect behaviour, testing redirects from multiple geographic locations after configuration changes to verify new rules have propagated.
Redirect chains and caching: each redirect hop in a redirect chain has its own cache headers and cache lifetime. A three-hop redirect chain might have the first hop cached in the browser, the second hop cached at the CDN, and the third hop going to the origin. Understanding the caching behaviour at each hop is important for managing redirect chain updates, a change to the first hop may not propagate immediately if it is cached.
Cache-Control for different content types
Different content types warrant different caching strategies, matching the cache duration to the content’s rate of change.
Static assets, long cache duration: CSS, JavaScript, fonts, and images that are versioned, content-hash in filename, can be cached for very long periods, up to one year. Cache-Control: max-age=31536000, immutable. When content changes the filename changes, new filename, new URL, new cache entry. Old versioned URLs that are no longer referenced expire naturally.
HTML pages, short cache duration: page HTML changes when content is updated. Cache-Control: max-age=300, must-revalidate: 5 minutes, balances freshness with reduced origin load. Frequently updated pages, news, live data, may use no-cache to ensure every load reflects the latest content.
API responses, varies by endpoint: static API data, reference lists, configuration, can be cached with moderate durations. Dynamic API data, user-specific, real-time, should use private, no-cache or no-store.
Redirect responses, short to moderate: Cache-Control: max-age=3600 for redirects that may change, allowing quick propagation of destination updates. Cache-Control: max-age=31536000 for redirect responses that are truly permanent and will never change destinations, such as a brand domain that will always redirect to the primary domain.