no‑cache vs must‑revalidate: Real‑World Tests Reveal Their True Behavior
This article experimentally compares the HTTP Cache‑Control directives no‑cache and must‑revalidate in both direct and proxy‑mediated scenarios, showing how browsers, cache servers, and origin servers interact and what status codes are returned under different cache‑expiration and resource‑change conditions.
Introduction
Front‑end developers familiar with the HTTP protocol often encounter the
Cache-Controlheader. Among its many directives, this article focuses on two that are frequently used and easily confused:
no-cacheand
must-revalidate.
Cache-Control: no-cache Cache-Control: max-age=60, must-revalidate
Readers can jump to the conclusion section if they are only interested in the final comparison.
no‑cache and must‑revalidate Overview
no-cache: The browser or cache server must revalidate the resource with the origin server before using a local copy, regardless of its freshness.
must-revalidate: A cached copy may be used while it is fresh; once it expires, the client must revalidate with the origin server.
The three participants in the caching process are browser , cache server , and origin server .
Components
Browser – initiates the request.
Origin server – provides the actual resource.
Cache server – sits between the browser and origin server, optionally serving cached copies.
Cache servers are optional; browsers can communicate directly with the origin server.
Cache servers accelerate resource access and reduce load on the origin server by storing and serving copies of resources.
Test Scenarios and Environment
Scenarios
Two scenarios are compared:
Browser accesses the origin server directly.
Browser accesses the origin server through a Squid cache server.
Environment
OS: macOS 10.11.4
Browsers: Chrome 52, Firefox 49
Cache server: Squid 3.6
Origin server: Express 4.14.0
Experiment code can be cloned from the GitHub repository
git clone https://github.com/chyingp/tech-experiment.gitand then
cd 2016.10.25‑cache-control/ && npm install.
<code>git clone https://github.com/chyingp/tech-experiment.git
cd tech-experiment/2016.10.25-cache-control/
npm install</code>Squid is installed separately; optionally start Squid and configure the local HTTP proxy.
When testing the “browser → cache server → origin server” path, the proxy must be enabled.
Scenario 1: Browser → Origin Server
Start the origin server with:
<code>cd connect-directly
node server.js</code>Cache‑Control: no‑cache
Case 1 – Resource unchanged on second request
First request returns
Cache-Control: no-cache. The second request receives
304 Not Modified, indicating the resource is still valid.
<code>HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
ETag: W/"b-s0vwqaICscfrawwztfPIiA"
...</code> <code>HTTP/1.1 304 Not Modified
X-Powered-By: Express
Cache-Control: no-cache
ETag: W/"b-s0vwqaICscfrawwztfPIiA"
...</code>Case 2 – Resource changes on second request
The query parameter
change=1forces the server to return different content each time. The second request receives
200 OKwith a new ETag.
<code>HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-cache
ETag: W/"b-8n8r0vUN+mIIQCegzmqpuQ"
...</code> <code>HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-cache
ETag: W/"b-0DK7Mx61dfZc1vIPJDSNSQ"
...</code>Cache‑Control: must‑revalidate
Requests are made to
/must-revalidatewith optional
max-ageand
changeparameters.
max-age– freshness lifetime in seconds.
change=1– forces the resource to change.
Case 1 – Browser cache still fresh
With
max-age=10, the second request within 10 seconds is served from the browser cache without any network request.
<code>HTTP/1.1 200 OK
Cache-Control: max-age=10, must-revalidate
ETag: W/"10-dK948plT5cojN3y7Cy717w"
...</code>Case 2 – Cache expired, resource unchanged
After 10 seconds the browser revalidates and receives
304 Not Modified.
<code>HTTP/1.1 304 Not Modified
Cache-Control: max-age=10, must-revalidate
ETag: W/"10-dK948plT5cojN3y7Cy717w"
...</code>Case 3 – Cache expired, resource changed
With
max-age=10&change=1, the second request after expiration receives
200 OKand a new representation.
<code>HTTP/1.1 200 OK
Cache-Control: max-age=10, must-revalidate
ETag: W/"new-etag"
...</code>Scenario 2: Browser → Cache Server → Origin Server
When a Squid proxy is in the path, the behavior of the two directives diverges.
Cache‑Control: no‑cache
Case 1 – First request results in
TCP_MISS/200in Squid logs.
<code>1477501799.573 17 127.0.0.1 TCP_MISS/200 299 GET http://127.0.0.1:3000/no-cache - HIER_DIRECT/127.0.0.1 text/html</code>Case 2 – Second request, resource unchanged yields
TCP_MISS/304, meaning Squid consulted the origin server and got a 304.
<code>1477501987.785 1 127.0.0.1 TCP_MISS/304 238 GET http://127.0.0.1:3000/no-cache - HIER_DIRECT/127.0.0.1 -</code>Case 3 – Second request, resource changed results in
TCP_MISS/200and the new content is returned.
<code>1477647837.216 1 127.0.0.1 TCP_MISS/200 299 GET http://127.0.0.1:3000/no-cache? - HIER_DIRECT/127.0.0.1 text/html</code>Cache‑Control: must‑revalidate
Case 1 – Cached copy present and fresh – a second browser (Firefox) receives the cached copy with
TCP_MEM_HIT/200.
<code>1477648947.594 5 127.0.0.1 TCP_MISS/200 325 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 text/html
1477649012.625 0 127.0.0.1 TCP_MEM_HIT/200 333 GET http://127.0.0.1:3000/must-revalidate? - HIER_NONE/- text/html</code>Case 2 – Cached copy expired, origin unchanged – Squid returns
TCP_MISS/304, so the browser gets a 304.
<code>1477649429.105 11 127.0.0.1 TCP_MISS/304 258 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 -</code>Case 3 – Cached copy expired, origin changed – Squid returns
TCP_MISS/200, delivering the new representation.
<code>1477650702.807 8 127.0.0.1 TCP_MISS/200 325 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 text/html
1477651020.516 4 127.0.0.1 TCP_MISS/200 325 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 text/html</code>Comparison Summary
When the cache server is not involved,
no-cachealways forces a revalidation (resulting in 304 if unchanged, 200 if changed), while
must-revalidateallows the browser to use a fresh local copy without contacting the server (200 from cache) and only revalidates after expiration.
When a cache server is present,
no-cachestill triggers a revalidation through the proxy, producing 304 or 200 depending on the origin’s state.
must-revalidatecan serve cached copies directly from the proxy if they are still fresh; otherwise the proxy revalidates and returns either 304 or 200.
Final Thoughts
The experiments show that
no-cacheand
must-revalidatebehave differently in both direct and proxy‑mediated contexts, especially regarding when revalidation occurs and which component (browser or proxy) performs it. Further tests could explore combinations with
max‑stale,
proxy-revalidate, and the impact of different proxy caching algorithms.
Related Links
RFC 2616 §14.9 – Cache‑Control: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.