Problem and When It Applies
A web application has four kinds of GET responses:
/index.htmlhas a stable URL, changes with each deployment, and references the current static assets./assets/app.8f3c.jshas a content hash in its filename, so a content change also changes the URL./api/catalogis a public product catalog. The server selects a representation fromAccept-Language, and the product allows roughly 60 seconds of staleness./api/cartis personalized for the authenticated user and must not be reused by a CDN or another user.
After one release, some users keep old HTML and request an old JavaScript file that the deployment already deleted. The catalog occasionally appears in the wrong language. The team also treats no-cache as “do not store,” so its proposed fixes do not describe the intended behavior. Explain how a private browser cache, a shared CDN cache, freshness, revalidation, and cache keys interact. Then design the response headers, deployment order, and verification method for all four responses.
The 60-second value is an interview assumption, not a production measurement from a source. The base scope covers the HTTP cache. Service Worker Cache, application memory caches, and framework data caches appear in follow-ups. This is a frontend, full-stack, or web-performance question whose core skill is reasoning about browser and HTTP platform behavior, so its category is frontend.
What the Interviewer Is Evaluating
The first signal is whether the candidate derives policy from four questions: may the response be stored, who may reuse it, how long is it fresh, and what happens after it becomes stale? Memorizing Cache-Control directives often mixes storage with direct reuse. A strong answer defines the data boundary before writing headers.
The second signal is an exact distinction between no-cache and no-store. no-cache permits storage but requires validation with the origin before each reuse. no-store tells caches not to store the response. Applying no-store to every dynamic response loses conditional-request benefits and some browser capabilities; it belongs where the requirement actually forbids HTTP-cache storage.
The third signal is a correct freshness and validation model. A fresh response can usually be reused without contacting an upstream server. When it is stale or validation is required, the client can send If-None-Match or If-Modified-Since. If the representation has not changed, the server returns 304 Not Modified; the cache updates metadata and reuses its stored body.
Finally, the interviewer looks for deployment and cache-key reasoning. Perfect static-asset headers cannot repair a release process in which old HTML points to deleted files. Omitting Vary: Accept-Language can also let a shared cache use a Chinese representation for an English request to the same URL. A strong answer covers headers, asset lifetime, representation variants, and reproducible tests.
Clarifying Questions Before Answering
- Do all four responses pass through a CDN?
s-maxagecontrols shared caches and does not set private-browser freshness. Without a CDN, the public API policy can be simpler. - Does the catalog language come from the URL or a request header?
/api/catalog?locale=zh-CNalready uses a distinct URI as part of the cache key. A single URL whose representation depends onAccept-Languageneeds an appropriateVaryfield. - May the browser store the cart response and validate it later? If a single user's private cache may store it, use
private, no-cache. If security or product policy forbids any HTTP-cache storage, useno-store. - How long can old hashed assets remain available? Immediate deletion breaks old tabs, history navigation, staggered deployments, and rollback versions. Retention must cover the period in which an old entry point remains reachable plus the rollback window.
- How stale may catalog data become? “At most 60 seconds” must distinguish normal freshness from extra stale use during revalidation or errors. If older content is never acceptable,
stale-while-revalidateandstale-if-errorcannot extend reuse. - Is a Service Worker installed? A Service Worker can intercept the request and return a Cache Storage entry before the normal HTTP cache participates. It must be inspected as a separate layer.
30-Second Answer Framework
“For each response I first decide whether it may be stored, who may reuse it, how long it is fresh, and how it is validated after that. A content-hashed JavaScript asset gets a one-year max-age plus immutable, with a guarantee that bytes at the same URL never change. HTML gets no-cache and an ETag, so the browser may store it but validates it on a normal reuse. The public catalog can give the CDN s-maxage=60, add an ETag, and use Vary: Accept-Language when the same URL serves multiple languages. The cart is at least private; use private, no-cache if browser storage is allowed, and no-store only when storage itself is forbidden. Deploy new assets before HTML and retain old hashed files. Then test an uncached 200, a fresh hit, 304 revalidation, language variants, and replay of old HTML.”
Step-by-Step Deep Dive
Step 1: Model storage, reuse, freshness, and validation separately
HTTP caching is not a single switch. A request path requires at least four decisions:
- Storage:
no-storeforbids storage. Other responses are subject to method, status, authorization, and explicit caching rules. - Reuse scope: A browser is a private cache that normally serves one user. CDNs and proxies are shared caches.
privateprevents shared-cache reuse, whilepubliccan explicitly allow it. - Freshness:
max-age=Nnormally defines how long private and shared caches can reuse a response directly. In a shared cache,s-maxage=Noverridesmax-age. Current response age also reflects metadata such asDateandAge. - Behavior after staleness: A cache can send a conditional request using a validator. An unchanged representation produces a 304 and reuse of the stored body; a changed representation produces a 200 with new content.
“Cache hit” therefore describes two different costs. A fresh hit needs no upstream confirmation. A revalidated hit still incurs a network round trip and validation work, although it avoids transferring the full response body. An interview answer should name which one it means.
Step 2: Give content-hashed static assets a long freshness lifetime
If the build guarantees that changed content receives a new URL, a static asset can return:
Cache-Control: public, max-age=31536000, immutable31536000 seconds is one year. immutable says the resource will not change while it is fresh and can avoid pointless validation in some reload paths. The important part is not the particular one-year number. It is the pair of invariants:
- The bytes at a given hashed URL never change.
- A new version uses a new URL referenced by new HTML.
Overwriting a file at the same long-lived URL leaves clients with old content. Deleting old hashed files also breaks old tabs, browser history, staggered edge nodes, and rollback releases that can still request them. A robust release uploads new assets first, verifies that they are reachable, publishes HTML afterward, and retains old hashed assets until old entry points and rollback versions are no longer reachable.
Step 3: Revalidate the stable HTML URL on every normal reuse
The entry HTML cannot naturally change its URL with each representation, so it can return:
Cache-Control: no-cache
ETag: "index-v184"The browser may store the document, but a normal reuse requires validation. A later request includes:
If-None-Match: "index-v184"The server returns 304 if the representation is unchanged, or 200 with the new HTML and a new ETag after a release. An ETag identifies a version of a representation. It does not have to be a content hash, but it must change when the representation changes. The response may also carry Last-Modified. If a request contains both If-None-Match and If-Modified-Since, the more precise ETag condition takes precedence under HTTP semantics.
no-cache does not mean “do not save.” Use no-store only if the product requires the HTML not to enter an HTTP cache at all, accepting the loss of 304 revalidation. Browser back/forward cache can also restore a page snapshot without following the ordinary HTTP revalidation path, so “the Back button showed an old page” cannot be diagnosed from Cache-Control alone.
Step 4: Control browser and CDN behavior independently for a public API
Suppose the catalog normally tolerates 60 seconds of delay. The browser should confirm on every request, while the CDN may directly reuse a response for up to 60 seconds:
Cache-Control: public, max-age=0, s-maxage=60
ETag: "catalog-v927"
Vary: Accept-LanguageEach directive has a separate job:
max-age=0lets the browser store the response but makes it immediately stale, so later use enters a validation path.s-maxage=60lets a shared cache reuse it directly for 60 seconds.ETaglets a browser or CDN make a conditional request after staleness.Vary: Accept-Languagetells a cache to include the language request header when selecting a stored response.
If the product accepts another 30 seconds of stale data in exchange for lower validation latency, append stale-while-revalidate=30. This standard directive affects every cache that supports it, not only the CDN. If only CDN behavior should change, use an explicit CDN-specific setting from the selected provider. When the business guarantee says content must never be older than 60 seconds, do not add this stale window. It is an explicit trade of freshness for lower latency, not a default optimization.
It is often more predictable to put locale in the URL, such as /api/catalog?locale=zh-CN. Logging, warming, invalidation, and cache keys then become explicit, and the response no longer needs Accept-Language to distinguish representations at one URL. Avoid casually adding Vary: User-Agent; its high number of possible values can destroy shared-cache reuse.
Step 5: Protect the personalized cart
The cart must never be served from a shared cache to another user. If the browser may store the current user's response but must confirm it before reuse, return:
Cache-Control: private, no-cache
ETag: "cart-v19"private limits storage to private caches. no-cache requires validation before reuse. Do not try to isolate users with Vary: Cookie: cookie values have extremely high cardinality and create both cache-key and privacy risks. Personalized responses should explicitly prevent shared reuse.
If the security requirement is “the response must not be written to any HTTP cache,” use:
Cache-Control: no-storeThere is no need to append private, no-cache, max-age=0. Overlapping or more restrictive directives do not make the policy safer; they make its intent harder to audit. Also, adding no-store to future responses does not erase previously stored fresh copies. Incident response may need a CDN purge, a versioned URL, expiry of old entries, or a targeted browser-clearing mechanism.
Step 6: Bind cache policy to a deployment protocol
The deleted JavaScript failure cannot be repaired by headers alone. The deployment protocol should:
- Upload every new hashed asset.
- Verify from production edge locations that each asset is reachable and has the correct MIME type, compression, and headers.
- Publish the HTML that references the new URLs.
- Retain old hashed assets through old-tab, staggered-release, and rollback windows.
- Restore old HTML during a rollback while its old assets still exist.
- Delete an asset only after no reachable entry point references it.
If multiple nodes generate HTML, their ETags must also correspond to the representation. Different ETags for identical content produce unnecessary 200 responses. Reusing the same strong ETag for different content can produce an incorrect 304. Compression, language, and other representation changes also need correct cache keys or Vary handling.
Step 7: Verify a sequence of requests instead of one reload
Build a request matrix in browser DevTools or with curl:
curl -I "$ORIGIN/index.html"
curl -I -H 'If-None-Match: "index-v184"' "$ORIGIN/index.html"
curl -I -H 'Accept-Language: zh-CN' "$ORIGIN/api/catalog"
curl -I -H 'Accept-Language: en-US' "$ORIGIN/api/catalog"Set ORIGIN to the target site's origin before running the commands. Verify all of the following:
- An initial request returns 200, the intended headers, and validators.
- A static asset does not contact upstream while fresh, and a changed URL retrieves a new file.
- An unchanged HTML conditional request returns 304, while a deployment produces 200 with a new ETag.
Ageincreases on catalog CDN hits, and validation occurs after shared freshness expires.- The two language requests receive different bodies and the response includes
Vary. - The cart has no configuration that permits shared-cache reuse.
- Replaying old HTML still retrieves its referenced hashed asset with 200.
- DevTools “Disable cache” is useful for network diagnosis but does not replace testing the real cache path.
Test an ordinary reload, a force reload, a new tab, back/forward navigation, and the path with a Service Worker separately. A force reload changes request cache directives, so testing only that path does not reproduce normal user behavior.
High-Quality Sample Answer
“I would not begin by listing headers. For each response I would answer four questions: can it be stored, who can reuse it, how long is it fresh, and how is it validated after that?
The JavaScript file is content hashed, so its URL changes with its bytes. I would return public, max-age=31536000, immutable and make ‘never overwrite content at the same URL’ a release invariant. The entry HTML has a stable URL, so I would use no-cache with an ETag. It can be stored, but a normal reuse validates it; unchanged content gets a 304, while changed content gets new HTML and a new ETag. no-cache permits storage. no-store is the directive that forbids it.
The public catalog permits about 60 seconds of delay. I would use max-age=0 for the browser and s-maxage=60 for the CDN. If the product accepts another 30 seconds of stale data in exchange for hiding revalidation latency, I would add stale-while-revalidate=30; otherwise I would omit it. If the same /api/catalog URL varies by Accept-Language, it needs Vary: Accept-Language. Putting locale in the URL is even easier to reason about.
The cart is explicitly user-private. If browser storage and validation are acceptable, I would return private, no-cache. If policy forbids storage, I would return only no-store. I would not allow the personalized response in a shared cache or use a high-cardinality cookie as its shared cache key.
Old HTML requesting a deleted bundle also exposes a deployment-protocol bug. I would upload new assets first, publish HTML second, and retain old hashed assets through the old-entry and rollback window. My verification would exercise an initial 200, a fresh hit, ETag-based 304, a post-deploy 200, two language representations, the cart's shared-cache boundary, and replay of old HTML—not just one force reload.”
Common Mistakes
- Interpreting
no-cacheas no storage → it permits storage and requires validation before reuse → useno-storeto forbid storage, or keepno-cacheplus a validator to benefit from 304. - Giving every resource a one-year freshness lifetime → stable-URL HTML can keep pointing to an old release → reserve long freshness for content-addressed assets and validate the entry document.
- Deleting old hashed assets immediately after release → old tabs, history, and rollback releases can still reference them → publish assets before the entry point and retain them through the reachable window.
- Setting an ETag without a freshness policy → the cache lacks a clear direct-reuse lifetime and may validate unnecessarily → define freshness and validation together.
- Calling a 304 free → it saves the response body but still costs a round trip and validation work → choose a fresh lifetime when latency and data tolerance justify it.
- Serving multiple languages at one URL without
Vary→ a shared cache can reuse the wrong representation → put language in the URL or addVary: Accept-Languageto the cache key. - Using
Vary: Cookieto isolate carts → high-cardinality keys reduce hits and increase privacy risk → prevent shared reuse withprivateorno-store. - Assuming a newly added
no-storedirective erases old entries → the new header cannot reach a fresh response that is still reused directly → purge it, version it, or wait for the old entry to expire. - Testing only with DevTools force reload → force reload changes request cache directives → test ordinary navigation, fresh hits, revalidation, history restoration, and Service Worker paths.
Follow-up Questions
Follow-up 1: May the catalog serve stale data when the CDN or origin fails?
Split the business tolerance into two windows: at most 60 seconds during normal operation, and a separate allowance during origin 5xx responses. If older data is acceptable during an error, evaluate stale-if-error with a bounded duration. If prices or inventory cannot be older, expose the error instead. stale-while-revalidate hides background revalidation latency; stale-if-error permits stale reuse during failure. They solve different problems.
Follow-up 2: When should the server use a strong or weak ETag?
A strong ETag means two representations are byte-for-byte equivalent and is appropriate where exact range requests or byte identity matters. A weak ETag begins with W/ and represents semantic equivalence despite possible byte differences, which can suit irrelevant formatting changes in server-rendered output. Cache validation can use either, but If-Match concurrency control and range requests require another check of strong-comparison rules.
Follow-up 3: Why does DevTools say “from memory cache” or “from disk cache” without showing a 304?
A fresh response can be reused without any conditional request, so there is no 304. Memory and disk describe storage choices made by the browser, not separate HTTP semantics. Inspect Cache-Control, Age, whether a request was sent, and the response's current age instead of inferring the whole policy from one DevTools label.
Follow-up 4: Why did changing response headers not help after adding a Service Worker?
The Service Worker may return an old Cache Storage response before a network request occurs, so the ordinary HTTP cache never participates. Inspect the fetch handler, Cache Storage version, activate-time cleanup, and client-claim timing. Version cache names or the resource manifest, then verify how pages controlled by an old Service Worker upgrade.
Follow-up 5: How should personalized HTML that references hashed assets be cached?
The HTML can use Cache-Control: private, no-cache to prevent shared reuse and validate before private-cache reuse. Hashed static assets that are identical for every user can still use public long-term caching. An authenticated page and its static resources do not need one shared policy; set the boundary from whether each representation is personalized.
Follow-up 6: How can a deployment safely delete old static assets?
Build a reference set from current HTML, rollback-capable HTML, route manifests, and release records, then add the maximum old-page reachability window. A hashed asset becomes eligible for delayed deletion only after no publishable entry point references it, the rollback window has ended, and access logs show no valid requests. The cleanup job should be stoppable and retain several recent releases so one bad decision does not destroy rollback.