How HTTP Caching Works: Cache-Control, ETags, and CDN Edge Caching

HTTP caching is the mechanism by which browsers, proxies, and CDN edge servers store copies of HTTP responses and serve them to subsequent requests without contacting the origin server. Caching is what makes the web fast. Without it, every page load, every image, every API response would require a full round trip to the origin server — adding latency, consuming bandwidth, and increasing server load. The HTTP caching model, defined in RFC 9111 (which supersedes the caching sections of RFC 7234 and RFC 2616), provides a rich set of directives that give origin servers fine-grained control over what gets cached, for how long, by whom, and under what conditions responses can be served stale.

The Cache-Control Header

Cache-Control is the primary mechanism for controlling HTTP caching behavior. It appears in both responses (from the server) and requests (from the client), though response directives are far more commonly used. Each directive controls a specific aspect of caching behavior:

Response Directives

# Cache for 1 hour in all caches (browsers, CDNs, proxies)
Cache-Control: public, max-age=3600

# Cache only in the browser, not in shared caches (CDNs/proxies)
Cache-Control: private, max-age=3600

# CDN caches for 1 day, browser caches for 5 minutes
Cache-Control: public, max-age=300, s-maxage=86400

# Do not cache at all
Cache-Control: no-store

# Cache but always revalidate before using
Cache-Control: no-cache

# Cache, but serve stale while revalidating in background
Cache-Control: public, max-age=600, stale-while-revalidate=300

# Immutable content (never revalidate, even on reload)
Cache-Control: public, max-age=31536000, immutable

The directives break down as follows:

HTTP Cache Decision Flow Browser Private Cache CDN Edge Shared Cache (s-maxage) Origin Authoritative Browser Cache HIT max-age not expired → serve immediately (0ms) Stale-While-Revalidate Serve stale, revalidate in background CDN Cache HIT s-maxage not expired Conditional Request If-None-Match: "etag" → 304 Not Modified CDN Revalidates s-maxage expired, check origin Cache MISS → full origin fetch

ETag and Conditional Requests

When a cached response becomes stale, the cache does not necessarily need to download the full response again. Conditional requests allow the cache to ask the origin server: "Has this resource changed since I last fetched it?" If not, the server responds with 304 Not Modified (no body), and the cache updates the freshness metadata without transferring the response body.

Two validation mechanisms exist:

ETag / If-None-Match (Strong Validation)

An ETag (entity tag) is an opaque string that uniquely identifies a specific version of a resource. It is typically a content hash or a version identifier. The server sends it in the response:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
ETag: "a1b2c3d4e5f6"

{"asn":13335,"name":"CLOUDFLARENET","prefixes":1842}

When the cached response becomes stale, the client sends a conditional request with If-None-Match:

GET /api/as/13335 HTTP/1.1
If-None-Match: "a1b2c3d4e5f6"

If the resource has not changed (the current ETag matches), the server responds with:

HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"
Cache-Control: public, max-age=60

No body is transmitted. The cache marks its stored response as fresh for another 60 seconds. If the resource has changed, the server sends a full 200 OK response with the new content and a new ETag.

ETags can be strong (byte-for-byte identical: "a1b2c3d4") or weak (semantically equivalent: W/"a1b2c3d4"). Weak ETags indicate that the content is equivalent but not necessarily identical — useful when insignificant variations (whitespace, comment timestamps) should not invalidate the cache.

Last-Modified / If-Modified-Since (Weak Validation)

The Last-Modified header provides a timestamp-based validation mechanism. It is simpler but less precise than ETags — if a resource changes twice within the same second, the Last-Modified timestamp cannot distinguish between the two versions.

HTTP/1.1 200 OK
Last-Modified: Wed, 24 Apr 2026 12:00:00 GMT
Cache-Control: public, max-age=60

# Revalidation request:
GET /api/as/13335 HTTP/1.1
If-Modified-Since: Wed, 24 Apr 2026 12:00:00 GMT

# If unchanged:
HTTP/1.1 304 Not Modified

When both ETag and Last-Modified are present, caches should use ETag for validation (it is the stronger validator). Most CDNs and browsers follow this behavior.

The Vary Header

The Vary header tells caches that the response varies based on specific request headers. Without Vary, a cache stores one response per URL. With Vary, the cache stores separate responses for each unique combination of the specified header values.

# Response varies based on Accept-Encoding (gzip vs brotli vs identity)
Vary: Accept-Encoding

# Response varies based on Accept-Language
Vary: Accept-Language

# Response varies based on both (separate cache entries for each combination)
Vary: Accept-Encoding, Accept-Language

The most common use of Vary is Vary: Accept-Encoding, which tells caches to store separate compressed and uncompressed versions of a response. Without this, a cache might serve a gzip-compressed response to a client that does not support gzip, or vice versa.

Vary: Accept is important for content-negotiated APIs that serve JSON or HTML depending on the Accept header. Vary: Cookie or Vary: Authorization effectively makes the response uncacheable in shared caches, since these headers differ per user.

A common CDN pitfall: Vary: User-Agent creates a separate cache entry for every unique User-Agent string. Since there are thousands of unique User-Agent strings in the wild, this effectively disables caching. Some CDNs (Cloudflare, Fastly) normalize User-Agent into device classes (mobile, desktop, tablet) to make Vary: User-Agent practical.

CDN Edge Caching

Content Delivery Networks place cache servers at hundreds of locations worldwide, serving cached content from the edge server closest to the user. The CDN caching model builds on HTTP caching but adds additional layers:

Multi-Tier CDN Cache Architecture Client Edge POP L1 Cache ~85-95% hit rate Shield / Tier 2 Regional Cache ~50-80% of misses Origin Server Authoritative ~1-5% of requests Edge POP L1 Cache (different region) miss miss Each layer absorbs a fraction of requests, reducing load on layers behind it

Edge caching behavior is controlled primarily by s-maxage and CDN-specific headers. Each CDN also provides proprietary extensions:

The CDN-Cache-Control header (standardized as the CDN-Cache-Control targeted field in RFC 9213) allows origin servers to send caching directives specifically for CDNs that are ignored by browsers. This solves the problem of wanting different cache durations at the edge vs. the browser:

# Browser: cache for 60 seconds, CDN: cache for 1 day
Cache-Control: max-age=60
CDN-Cache-Control: max-age=86400

Cache Invalidation

Phil Karlton's famous quote — "There are only two hard things in Computer Science: cache invalidation and naming things" — remains painfully true. HTTP caching provides no standard mechanism for proactive invalidation. The Cache-Control model is fundamentally based on expiration: the origin sets a TTL, and caches serve stale content until the TTL expires. Explicit invalidation is not part of the HTTP caching specification.

In practice, several strategies exist:

URL fingerprinting is the most reliable invalidation strategy. By embedding a content hash or version in the URL (app.a1b2c3d4.js, /api/v2/data), you create a new URL for every new version. The old URL's cache entry becomes irrelevant because no client requests it. This is why modern build tools generate fingerprinted asset filenames. Combined with Cache-Control: public, max-age=31536000, immutable, fingerprinted URLs can be cached forever.

CDN purge APIs provide explicit invalidation at the CDN layer. Every major CDN offers APIs to purge specific URLs, URL patterns, or cache tags:

# Cloudflare: purge specific URLs
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone}/purge_cache" \
  -H "Authorization: Bearer {token}" \
  -d '{"files":["https://god.ad/api/as/13335"]}'

# Fastly: purge by surrogate key (tag)
curl -X POST "https://api.fastly.com/service/{id}/purge/as-13335" \
  -H "Fastly-Key: {token}"

Tag-based purging (Fastly's Surrogate-Key, Cloudflare's Cache-Tag) is the most powerful CDN invalidation mechanism. The origin tags responses with identifiers representing the data they contain. When data changes, the origin purges all responses tagged with that identifier. For a BGP looking glass, a route change for AS 13335 could purge all cached responses tagged as-13335, regardless of URL.

Short TTLs with stale-while-revalidate provide a middle ground. By setting max-age=60, stale-while-revalidate=3600, users always get instant responses (from cache), while the cache updates in the background within 60 seconds of a change. This trades perfect consistency for performance.

Caching API Responses

API responses present unique caching challenges compared to static assets. The same URL may return different content based on authentication, query parameters, request headers, or server-side state that changes unpredictably.

Effective API caching strategies:

Browser Cache Behavior

Browser caching is more nuanced than the specification suggests. Different types of navigation trigger different caching behavior:

Browsers also implement heuristic caching: when a response has no Cache-Control or Expires header but does have a Last-Modified header, browsers cache the response with an implicit max-age of 10% of the time since the resource was last modified. This means a resource last modified 100 days ago gets a heuristic max-age of 10 days. This can cause surprising staleness for resources where the server forgot to set explicit cache headers.

Cache Busting and Versioning Strategies

For static assets (JavaScript, CSS, images, fonts), the standard approach is a two-tier strategy:

  1. HTML pages: Cache-Control: no-cache (always revalidate) or very short max-age. HTML is the entry point that references other assets; it must always point to current asset versions.
  2. Fingerprinted assets: Cache-Control: public, max-age=31536000, immutable. Since the filename changes when the content changes, caching forever is safe. Build tools (webpack, Vite, esbuild) generate these filenames automatically.

This pattern ensures users always get the latest HTML (with fresh asset references) while benefiting from permanent caching of unchanged assets. The initial HTML load revalidates with a 304 most of the time, while all referenced assets are served from cache with zero network overhead.

For single-page applications like the one at god.ad, the HTML shell is served with short cache durations while the embedded JavaScript and CSS (included directly in the HTML in this case) benefit from the HTML's own versioning through deployment-time updates.

Caching and DNS

DNS has its own caching layer (TTLs on DNS records) that interacts with HTTP caching in important ways. When you update DNS to point to a new server (during a migration or failover), clients with cached DNS records continue to connect to the old server. If the old server is down, users experience failures even if the new server is healthy. CDNs mitigate this by providing stable anycast IPs that do not change during infrastructure changes — the CDN handles routing to the correct origin internally.

For DNS-based load balancing, short DNS TTLs (30-60 seconds) conflict with HTTP caching: even if the HTTP cache is fresh, the client may need to re-resolve DNS for the next request. The interaction between DNS TTLs, HTTP cache TTLs, and CDN edge TTLs creates a complex system where the effective cache duration is the minimum of all three.

Security Implications

Caching introduces several security considerations:

Summary

HTTP caching is a multi-layered system that spans browsers, CDN edges, reverse proxies, and origin servers. Cache-Control directives provide fine-grained control over freshness, storage, and revalidation. ETags enable efficient conditional requests that avoid re-transferring unchanged content. The Vary header handles content negotiation by maintaining separate cache entries per header combination. stale-while-revalidate provides the best of both worlds: instant cached responses with background freshness updates.

CDN edge caching amplifies these benefits by placing cached content close to users worldwide, but introduces complexity around cache invalidation, tag-based purging, and CDN-specific control headers. URL fingerprinting remains the most reliable invalidation strategy, and the immutable directive eliminates unnecessary revalidation on page reload.

The god.ad BGP Looking Glass uses Cloudflare CDN caching with appropriate cache headers for its static assets and edge-cached dynamic responses, including generated OG images and API lookups. You can inspect the caching headers on any response using your browser's developer tools or the cf-cache-status header in the response.

See BGP routing data in real time

Open Looking Glass
More Articles
How API Gateways Work: Routing, Auth, Rate Limiting, and Protocol Translation
How GraphQL Works: Schema, Queries, Resolvers, and Execution
How HTTP Cookies Work: Set-Cookie, Security Attributes, and SameSite
How Server-Sent Events (SSE) Work: HTTP Streaming for Real-Time Updates
What is BGP? The Internet's Routing Protocol Explained
What is an Autonomous System (AS)?