HTTP Caching
Outline
Key distinction
HTTP caching separates freshness from validation. The response directive no-cache allows storage but requires successful origin validation before reuse; no-store prohibits storage. The interview and screenshots below are from 2016. The explanation was checked against RFC 9111 on 2026-10-15; no new browser or CDN tests were performed.
I unexpectedly received an interview invitation from Alibaba DingTalk’s frontend team this afternoon, which made me quite happy. The interview covered everything from projects I’ve worked on to technologies I’ve used like Angular, React, and Express, making me realize my understanding isn’t deep enough. For example, I mentioned I like React and that large projects require Redux for data management. Then I was asked what problems Redux actually solves, and I couldn’t answer clearly. Overall, though, I felt pretty good about the first part of the interview.
But towards the end, the interviewer asked a rather fundamental question: “How do you control caching in the HTTP protocol?” I was stumped; I rarely encounter this in my daily work. I could only guess, mentioning changing filenames and setting relevant parameters in HTTP headers, but I couldn’t explain the specifics of how to set them or what content to include. I felt certain I’d fail at this point. But as the saying goes, one shouldn’t stumble twice in the same place. I’m writing this article to document the relevant aspects of HTTP caching.
Overview
HTTP caching is primarily controlled in the following ways:
- Expires
- Cache-Control
- Last-Modified/If-Modified-Since
- ETag/If-None-Match
Let’s explore each one in detail.
Expires
Expires sets an absolute expiry time and remains valid in modern HTTP. A fresh, reusable response can normally be used directly, but reload behavior or request directives may still require a network request. Applicable Cache-Control freshness directives take precedence. These headers do not form a single universal priority order, and the historical screenshot does not establish the current blog configuration.
The image above shows the HTTP header for a JavaScript file on my blog. As you can see, the Expires field (red box 1) is set to 4 hours after the current date. This means that, under normal circumstances, requests for this file before the Expires timestamp will directly use the local cache instead of re-fetching from the server.
Cache-Control
Cache-Control and Expires serve essentially the same purpose: to indicate the validity period of the current resource, thereby controlling whether the browser uses a local cache or re-fetches the resource from the server. However, Cache-Control offers more granular control, and when both Expires and Cache-Control are present in the header, Cache-Control takes precedence.
The following describes response directives; request directives have different roles.
| Directive | Meaning |
|---|---|
| public | Permits shared storage that might otherwise be restricted, subject to other directives |
| private | Prohibits shared-cache storage; private browser storage remains possible |
| no-cache | Storage is allowed, but successful origin validation is required before reuse |
| no-store | A cache must not store the response |
| max-age=N | Sets a freshness lifetime of N seconds; age calculation also considers Date and Age |
| s-maxage=N | Sets shared-cache freshness, overriding its max-age or Expires |
| must-revalidate | A stale response cannot be reused without successful validation |
The request directives min-fresh and max-stale express required remaining freshness and acceptable staleness, subject to response restrictions.
As shown, the Cache-Control field (red box 2) has a value of public, max-age=14400. This means its validity period is 14400 seconds, or 4 hours, which is the same as the Expires time above. However, unlike Expires, it doesn’t require a specific timestamp like Mon, 07 Mar 2016 17:14:33 GMT; it only needs a maximum age. This is likely a more commonly used method.
Last-Modified/If-Modified-Since
Last-Modified supplies a time validator. A client validating its stored response can send If-Modified-Since. For ordinary GET/HEAD requests, an unchanged representation can receive 304, allowing reuse of the stored body; a changed representation normally receives 200 with a new body.
Validators do not require Cache-Control, and validation is not limited to max-age expiry. No-cache, request directives or reload behavior may require it too. Without explicit freshness information, caches may use heuristic freshness under the HTTP rules.
ETag/If-None-Match
ETag is a server-provided validator for a selected representation, not necessarily a file hash. A client can send the stored tag in If-None-Match. For GET/HEAD, a match normally yields 304; a mismatch normally yields 200 and a new body. Max-age is not a prerequisite.
Image from The diagram combines freshness decisions and conditional validation. If both If-None-Match and If-Modified-Since are present, If-None-Match is evaluated first. Consider browser and shared CDN caches separately; do not copy a public-cache example onto personalized responses. HTTP caching is distinct from Service Worker Cache API storage, application caches and browser history navigation caching.
Reference Articles:
- Browser Caching Mechanism
- Introduction to Browser Cache-Related HTTP Headers: Expires, Cache-Control, Last-Modified, ETag
- HTTP Request Header Tag If-Modified-Since