Edge caching, explained plainly

In this article · 15 sections
- The three places a response may live
- Fresh, stale, and revalidated are different
- Cache-Control is a contract, not a suggestion
- Shared, private, authenticated, and personalized
- Cache keys decide equivalence
- Vary, content negotiation, and compression
- Stale service can protect availability
- Purge, invalidation, and versioned URLs
- Stampedes, negative caching, and origin protection
- Security and privacy failure modes
- Roll out caching as a controlled change
- Measure what the cache is actually doing
- A practical troubleshooting sequence
- Conclusion
- References and further reading
A cache is a copy with rules. It keeps an earlier HTTP response so a later, equivalent request can be answered without doing all the work again. The useful part is not simply putting files near readers; it is preserving HTTP meaning while deciding when a copy is safe to reuse.
That distinction matters because a cache can improve capacity and availability, yet can also disclose one person’s response to another, freeze an error, or serve an older release after a deployment. This guide follows one request from browser to CDN to origin, then turns the protocol into design and troubleshooting decisions. It uses the terminology and requirements of RFC 9110 and RFC 9111, both published in June 2022; RFC 9111 obsoletes RFC 7234.
Open the full-size diagramThe three places a response may live
Start with three layers, not with a CDN product. A browser has a private cache belonging to one user agent. A corporate proxy, reverse proxy, or CDN has a shared cache that may serve many users. Your application, web server, and databases are the origin: the authority expected to generate or retrieve the representation. RFC 9111 defines a cache as a store plus the machinery for storing, retrieving, and deleting response messages.
For a normal GET, the browser first asks whether its private cache has a usable response. If not, it sends the request to the configured network path. A CDN edge may find an entry whose cache key matches, and return it. On a miss, the edge asks the origin, stores the response if allowed, and forwards it. There can be more than one intermediary, so a response may pass through multiple shared caches before it reaches the browser.
A hit is therefore a statement about a particular cache and key, not a guarantee that every layer hit. An edge HIT can still be preceded by a browser miss; an origin request can still be avoided by an upper-level shield or regional cache. Model the path as a sequence of independent decisions, each constrained by the response headers and by local policy.
The cache exists to reuse an equivalent prior response. It does not make a POST safe to replay, turn personalized HTML into public content, or make a mutable database read immutable. Method, target URI, selected representation, authorization context, and response directives must line up before reuse is correct.
Fresh, stale, and revalidated are different
A stored response is fresh when its current age is within its freshness lifetime and it can be reused without asking the origin. Once it is stale, the cache normally needs validation before using it, although the protocol and explicit extensions can permit carefully bounded stale service. This is the central distinction: freshness is a protocol decision, while a provider’s label such as HIT is an implementation observation.
An origin can give a response an explicit lifetime with Cache-Control: max-age=300, or use Expires for an absolute HTTP date. A cache computes age from response timing and the Age field as it travels. max-age=0 does not mean “never store”; it means the response is immediately in need of validation. Google’s Cloud CDN documentation makes the same practical distinction: a zero TTL in a negative-caching policy is not the same thing as max-age=0 in a response header.
Revalidation asks whether the stored representation is still valid. The cache sends a conditional request, commonly with If-None-Match containing an origin-issued ETag, or If-Modified-Since based on Last-Modified. A 304 Not Modified lets the cache refresh metadata and reuse the stored body; a normal new response replaces it. Revalidation can preserve correctness while avoiding transfer of an unchanged representation, but it still consumes origin and network work.
Do not treat “stale” as synonymous with “wrong.” It means the stated freshness interval has elapsed. Whether a stale response may be served depends on request directives, response directives, failure conditions, and cache policy. Log the response age and revalidation result when investigating apparent old content.
Cache-Control is a contract, not a suggestion
The response directives have deliberately different jobs. no-store tells caches not to store the request or response. Use it for data that must not be retained by an HTTP cache, while remembering that application logs and other layers have separate retention rules. private says a response is intended for one user and must not be stored by a shared cache; a browser may still store it. public explicitly permits shared storage, including cases that would otherwise be restricted by normal rules.
no-cache is frequently misunderstood. It does not prohibit storage. It says that a stored response cannot be used for a subsequent request without successful validation. Thus private, no-cache can be a sensible browser policy for account pages, while no-store is the stronger retention prohibition. RFC 9111, section 5.2 defines these meanings.
max-age controls freshness for caches generally. s-maxage supplies a freshness lifetime for shared caches and overrides max-age or Expires there; browsers ignore it. It also carries proxy-revalidate semantics, so a shared cache cannot simply serve the response stale. must-revalidate applies that prohibition to caches generally, while proxy-revalidate targets shared caches. These are stricter than merely choosing a short TTL.
The stale extensions are opt-in. stale-while-revalidate=30 allows a cache to serve a response for up to that additional interval while it revalidates in the background. stale-if-error=300 permits a stale response during specified origin failures for the bounded period. RFC 5861, an Informational RFC published in May 2010, defines those extensions; RFC 9111 explains when stale responses are otherwise permitted. Check that your CDN implements the interaction you intend: for example, Cloudflare documents that must-revalidate, proxy-revalidate, s-maxage, or no-cache can prevent its stale-while-revalidate behavior.
A useful starting policy for a versioned public asset is Cache-Control: public, max-age=31536000, immutable, with the long lifetime justified by the content-addressed or versioned URL. For a public page that can tolerate brief old content, choose a measured max-age, add a bounded stale window only if the product accepts it, and retain validators. For personalized or sensitive output, choose private or no-store rather than hoping a CDN will infer intent.
Shared, private, authenticated, and personalized
Private caching is a browser-level optimization for one user. Shared caching is a cross-user optimization and therefore has a much higher proof burden. A response that embeds a name, cart, subscription state, permission, location, or experiment assignment must not be reused for a different context unless that context is represented safely in the cache key and the response policy permits sharing. In many applications the safer answer is to keep the HTML private and cache only public assets or an explicitly designed public shell.
Authentication needs special care. RFC 9111 places additional restrictions on storing responses to requests carrying Authorization; a shared cache needs an explicit permission such as suitable public, s-maxage, or must-revalidate semantics, and the application must still prove that cross-user reuse is safe. Do not interpret “the CDN can be configured to cache it” as evidence that it should be cached.
Cookies are request inputs, but their mere presence is not a complete cache design. A session cookie can make an otherwise identical URL produce different HTML. Some platforms bypass caching when a cookie is present; others can be configured to ignore or include selected cookies. Treat platform defaults as configuration to verify, not as a universal HTTP rule. If a cookie changes the representation, either make the response private, vary the cache key on a deliberately small and safe classification, or split the personalized fragment from a cacheable public response.
Avoid sending Set-Cookie on content intended for broad shared reuse unless the provider’s behavior and your policy are understood. Test both a first request with a cookie and a clean request without one. A dangerous failure is not merely a low hit ratio; it is a cache hit that returns one user’s data to another.
Cache keys decide equivalence
RFC 9111 says the minimum cache key includes the request method and target URI, although many caches primarily use the URI for GET. Real CDNs commonly let operators add selected query parameters, headers, and cookies. AWS describes the key as the unique identifier for an object and notes the trade-off: including fewer values generally increases hit probability, but only values that cannot change the representation may be omitted.
Design the key from representation differences. If ?page=2 changes a result, it belongs in the key. If a tracking parameter does not change content, normalize or remove it before cache lookup so that it does not create needless variants. Canonicalize equivalent spellings, constrain parameter order where your platform permits it, and never let an untrusted parameter influence the response without deciding whether it is keyed, validated, or ignored.
Do not add every request header or cookie “for safety.” That can create a huge number of variants, destroy reuse, and make invalidation difficult. Conversely, omitting a meaningful input creates cache confusion. Maintain a small written inventory: inputs that select representation, inputs forwarded only to origin, and inputs that are rejected or ignored.
A CDN may distinguish its cache key from its origin-request policy. CloudFront, for example, documents separate controls for what participates in the key and what is forwarded on a miss. Forwarding a header to the origin does not automatically make it a key dimension. Review both settings together.
Vary, content negotiation, and compression
Vary tells a compliant cache which request fields were used to select a representation. Vary: Accept-Encoding is common when the body differs between compressed and uncompressed forms. Language, media type, or client hints may also be legitimate dimensions, but each increases the variant set. If the origin varies on a field and the cache does not account for it, the cache can serve the wrong representation.
Vary: Cookie is technically expressive but often operationally disastrous because almost every browser has a distinct cookie string. Prefer a small, explicit variant such as a server-generated audience class, and key on that class only after it is trustworthy and non-sensitive. If the provider has special handling for Vary, read its documentation rather than assuming RFC behavior maps one-to-one to provider defaults. Cloudflare explicitly documents that it does not consider arbitrary Vary values by default, while allowing configured exceptions.
Compression is another variant boundary. A cache must not deliver bytes encoded for one request to a client that cannot decode them. Use the CDN’s supported compression behavior or ensure Vary: Accept-Encoding and key logic are consistent. Test content negotiation with controlled requests and inspect both the response body and headers.
Stale service can protect availability
Freshness trades origin contact for the possibility of older content. A short lifetime gives updates a narrower normal window but increases validation traffic. A long lifetime reduces origin work but makes publication and rollback depend on versioning or purge. There is no universally correct TTL; choose it from content volatility, correctness cost, origin capacity, and recovery objectives.
stale-while-revalidate is useful when serving the last known good page is better than making every reader wait for an origin round trip. The first request after expiry may trigger background validation, while readers receive the old representation during the allowed window. Ensure your origin can handle the revalidation and decide whether temporary old data is acceptable for the endpoint. Do not use this policy for an entitlement, balance, or other response where old state could cause harm.
stale-if-error can turn an origin outage into a visibly old but usable public response. Define what “error” means in the implementation and keep the stale window bounded. A cached 200 page should not silently mask a persistent deployment failure forever; expose age and status to operators, and provide a way to force validation during an incident.
Remember that explicit prohibitions win. must-revalidate, proxy-revalidate, s-maxage, and no-cache can make a provider refuse stale service even when stale extensions appear in the header. A policy that is correct in a local test may differ at another CDN or in a browser.
Purge, invalidation, and versioned URLs
Expiration waits for the object’s lifetime to end. Invalidation or purge asks an intermediary to remove or mark an object before then. Neither necessarily erases a copy already held in a browser or another corporate proxy. That is why emergency purge is not a substitute for a release strategy.
Use invalidation for urgent removal, security response, or a small set of mutable URLs, and make the operation observable: record who requested it, which paths or tags were targeted, when completion was reported, and how a clean request was verified. A wildcard purge is simple but can create a broad refill surge and hide mistakes. Confirm whether a provider invalidates by exact path, query-string variant, tag, or another namespace.
For frequently updated static files, versioned filenames or content hashes are usually easier to reason about. Publish app.abc123.js, change the HTML reference, and leave the old asset available until clients age out. AWS recommends versioning for frequent updates because it controls the URL even when an old copy remains in a user or corporate cache, and it simplifies rollback. The HTML shell still needs its own shorter policy or purge path.
Purge is also a security boundary. Restrict credentials, require an audit trail, and avoid exposing a public purge endpoint. A failed purge should be an actionable incident, not an assumption that “the edge will catch up.”
Stampedes, negative caching, and origin protection
A cache stampede occurs when many requests discover a miss or expiration together and all perform the expensive origin work. It can happen after a purge, deployment, regional failure, cache eviction, or a too-short TTL. The result is positive feedback: slower origin responses cause more requests to wait, retry, or expire, which adds still more load.
Use request coalescing or single-flight behavior so one request refreshes a key while others wait for that result or receive an explicitly permitted stale response. Add jitter to refresh schedules when you control proactive refresh. Bound origin concurrency per key or tenant, use timeouts and bounded retries, and fail fast when the origin is overloaded. Google’s SRE guidance warns that cold caches can turn a cache from a latency aid into a capacity dependency; it recommends testing empty-cache and restart conditions, gradually warming new capacity, and using general cascading-failure controls.
Negative caching stores a non-success result such as a 404 or selected redirect for a limited time. It protects the origin from repeated requests for a genuinely absent object and can reduce repeated work during a scan or link burst. It can also freeze a just-created resource as “not found,” so keep the TTL conservative and distinguish permanent absence from eventual consistency.
Google Cloud CDN documents status-specific negative-caching policies and notes that explicit origin Cache-Control or Expires can take precedence in some modes. Read the exact precedence for your provider. Never cache a transient authorization failure or a user-specific error in a shared key merely because it has a status code.
Security and privacy failure modes
Cache poisoning is the case where an attacker influences a response that a shared cache stores, magnifying the impact to later users. OWASP’s Cache Poisoning guidance describes the core pattern: an attacker gets a crafted response associated with a cache key, then other requests receive it until the entry is purged or replaced. Modern implementations differ, but the defense is consistent: normalize and validate inputs, make cache-key dimensions match response variation, reject ambiguous routing, and do not cache unsafe error or redirect behavior by accident.
Treat the Host header, forwarded host, scheme, path normalization, query parsing, and redirect target as security-sensitive. The origin and CDN must agree on which authority and URL identify the resource. Do not reflect arbitrary request headers into cacheable output. Set a deliberate policy for unknown methods, malformed encodings, and alternate URL spellings.
Privacy risks include cross-user disclosure, persistent identifiers in shared objects, and timing signals that reveal whether a private object is cached. Keep authentication and personalization out of shared responses by default. Separate public and private hostnames or paths when that makes the boundary clearer. When measuring hit ratio, avoid putting account identifiers, full URLs with secrets, or cookie values into logs.
HTTPS protects data in transit between each correctly configured hop; it does not make an incorrectly shared object private. Restrict origin access so the origin trusts only the intended edge path, validate purge authority, and review cache rules as part of access-control changes.
Roll out caching as a controlled change
Begin with an inventory of endpoints and classify each as public immutable, public mutable, private personalized, authenticated, or non-cacheable. For each class write the allowed cache layers, key dimensions, freshness policy, stale policy, validators, purge mechanism, and correctness owner. If nobody owns the answer to “may this response cross user boundaries?”, do not place it in a shared cache.
Start with static assets whose URLs are versioned and whose representation does not depend on cookies or authorization. Add validators and inspect headers before increasing TTL. Next test a low-risk public GET with synthetic content. Keep a bypass or rollback rule that stops shared reuse without requiring a full application release.
Test a matrix, not one curl command: cold browser and warm browser; edge miss, hit, and expired object; clean and cookie-bearing requests; different Accept-Encoding and language values; authenticated and anonymous users; query parameters in different orders; origin 304, 404, 500, timeout, and slow responses. Compare body, status, Age, ETag, Last-Modified, Cache-Control, Vary, and provider cache-status headers.
Make deployment safe by publishing new asset versions before referencing them, shortening mutable HTML lifetime before a risky migration, and rehearsing purge and rollback. If you change cache-key rules, expect old variants and new variants to coexist until their lifetimes end or are invalidated.
Measure what the cache is actually doing
Track edge requests, hit ratio, byte hit ratio, origin request rate, origin error rate, revalidation rate, object age, stale responses, purge completion, and cache-fill concurrency. Segment these by route, status, region, cache key class, and release. A global hit ratio can improve while an important authenticated endpoint is misconfigured; aggregate numbers need dimensions.
Measure correctness as well as efficiency. Use synthetic probes that embed a harmless version marker in public content and assert that the right variant is returned for each request class. Alert on a private response appearing at a shared layer, unexpected Set-Cookie, a sudden increase in cache-key cardinality, a rise in 304s after a supposed immutable release, or old content beyond its documented bound.
Do not promise a universal latency percentage. The result depends on geography, connection reuse, object size, protocol, origin distance, load, and whether the cache was warm. Report before-and-after distributions and origin work avoided, with the test population and cache state described.
A practical troubleshooting sequence
First preserve the exact request and response, including method, URL, query string, selected headers, cookies, authorization state, status, body marker, and timestamps. Redact secrets. Repeat from a clean client and a warm client, and record the edge location if the provider exposes it.
Second determine the layer that served the response. Inspect browser developer tools, Age, ETag, Date, Via, and provider cache-status headers. A browser hit means the CDN was never contacted; an edge hit means the origin was not contacted for that request. Do not diagnose an origin problem until you have proved the request reached it.
Third compare cache keys and variation inputs. Check path normalization, query handling, host, encoding, language, cookies, authorization, and Vary. Ask whether the cached body is actually wrong or merely old within the declared policy. If two requests should differ, confirm that their key differs or that the response is private.
Fourth inspect freshness and revalidation. Calculate the expected lifetime from max-age, s-maxage, Expires, provider overrides, and current Age. Look for a conditional request and a 304. Check whether must-revalidate, no-cache, or a provider rule blocks stale serving.
Fifth isolate invalidation and origin behavior. Verify the exact purge path or tag, completion status, and every layer that may retain a copy. Fetch the origin directly through a controlled hostname if possible, compare its validators and body marker, and check whether a 404 or 5xx was negatively cached.
Finally check load dynamics. If misses rose after a purge, expiration, deploy, or restart, look for coalescing, concurrency, retries, timeouts, cold-cache capacity, and origin saturation. Stabilize with a narrow bypass or bounded stale policy, then fix the key, header, or rollout cause.
Conclusion
Edge caching is disciplined reuse, not a magic switch. Decide what a response means, who may receive it, which request inputs select it, how long it remains fresh, how it is revalidated, and how it is removed. Keep shared caching narrow until those answers are testable. With explicit headers, small correct keys, bounded stale behavior, versioned releases, origin protection, and layer-by-layer measurements, a cache can reduce repeated work without becoming a hidden source of wrong or private responses.
References and further reading
- RFC 9110: HTTP Semantics
- RFC 9111: HTTP Caching
- RFC 5861: HTTP Cache-Control Extensions for Stale Content
- Cloudflare: Origin Cache Control
- Cloudflare: Revalidation
- Google Cloud: Use negative caching | Cloud CDN
- Amazon CloudFront Developer Guide: Control the cache key with a policy
- Amazon CloudFront Developer Guide: Invalidate files to remove content
- Google SRE Book: Addressing Cascading Failures
- OWASP: Cache Poisoning

Edge computing without the buzzwords: placement, state, and operations
Decide where edge workloads belong by balancing latency, state, residency, disconnected operation, orchestration and operational risk.

Operating DNS at the edge
A research-grounded guide to operating DNS with anycast: routing realities, node readiness, monitoring, capacity, deployment, and incident response.

Building DNS that survives bad days
What we learned designing DNS architecture that keeps resolving when providers, links and regions fail at the same time.