Frontend Development 18 min read

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.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
no‑cache vs must‑revalidate: Real‑World Tests Reveal Their True Behavior

Introduction

Front‑end developers familiar with the HTTP protocol often encounter the

Cache-Control

header. Among its many directives, this article focuses on two that are frequently used and easily confused:

no-cache

and

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.git

and 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=1

forces the server to return different content each time. The second request receives

200 OK

with 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-revalidate

with optional

max-age

and

change

parameters.

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&amp;change=1

, the second request after expiration receives

200 OK

and 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/200

in 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/200

and 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-cache

always forces a revalidation (resulting in 304 if unchanged, 200 if changed), while

must-revalidate

allows 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-cache

still triggers a revalidation through the proxy, producing 304 or 200 depending on the origin’s state.

must-revalidate

can 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-cache

and

must-revalidate

behave 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

cachingweb performancehttpcache controlmust-revalidateno-cache
Tencent IMWeb Frontend Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.