Caching static assets with ETag and Cache-Control headers isn’t about making things faster; it’s about making the network disappear for most users.
Let’s watch a browser fetch a style.css file that’s been cached.
Scenario:
-
First Request: Browser asks for
/static/style.css. Server responds with:HTTP/1.1 200 OK Content-Type: text/css Last-Modified: Tue, 15 Nov 2023 10:00:00 GMT ETag: "abcdef12345" Cache-Control: public, max-age=31536000, immutableThe browser downloads the file and stores it locally, along with the
ETagandCache-Controlheaders. Themax-age=31536000(one year) tells the browser it can reuse this file for up to a year without asking the server.immutableis a strong hint that this file will never change. -
Second Request (after a short time): Browser needs
/static/style.cssagain. Because it’s within themax-ageperiod, the browser doesn’t send a request to the server. It servesstyle.cssdirectly from its local cache. No network round trip, instant load. -
Third Request (after a long time, but still within
max-age): Browser needs/static/style.css. It still has the file cached. If theimmutableflag is present, the browser might not even check if it’s still valid until themax-ageis completely expired. This is the power ofimmutable. -
Request after
max-ageexpires (or ifimmutablewasn’t used and the browser revalidates): Browser needs/static/style.css. Themax-agehas passed. Now, the browser needs to revalidate if its cached copy is still fresh. It sends a conditional GET request:GET /static/style.css HTTP/1.1 Host: example.com If-None-Match: "abcdef12345" If-Modified-Since: Tue, 15 Nov 2023 10:00:00 GMTThe browser sends its cached
ETag(If-None-Match) andLast-Modifieddate (If-Modified-Since) to the server. -
Server Response to Revalidation:
- If the file hasn’t changed: The server sees the
ETagmatches and theLast-Modifieddate is the same. It responds with:
The browser receives this, knows its cached copy is still good, and serves it from local storage. This is a tiny response, saving bandwidth and time.HTTP/1.1 304 Not Modified - If the file has changed: The server would respond with a
200 OKand the new version ofstyle.css, along with newETagandLast-Modifiedheaders.
- If the file hasn’t changed: The server sees the
The mental model:
ETag(Entity Tag): This is a unique identifier for a specific version of a resource. Think of it like a file’s content hash (e.g., MD5, SHA-1). If the file content changes, theETagmust change. Web servers often generate this based on the file’s modification timestamp and size, or a true content hash.Last-Modified: This is the timestamp of when the resource was last changed on the server. It’s a fallback or complementary mechanism toETag.Cache-Control: This is the primary directive for controlling caching.public: Allows caching by intermediate proxies (like CDNs) and the browser.private: Only allows caching by the user’s browser.max-age=<seconds>: How long the resource is considered "fresh" in seconds.31536000is one year.no-cache: The resource can be cached, but the browser must revalidate with the origin server every time before using it.no-store: The resource must not be cached anywhere.immutable: A strong hint to the browser that the resource will never change. If used withmax-age, the browser can often skip revalidation entirely until themax-ageexpires. This is crucial for versioned assets (e.g.,style.a1b2c3d4.css).
When to use what:
- Static Assets (CSS, JS, Images, Fonts): These are prime candidates for aggressive caching. Use
Cache-Control: public, max-age=<large_number>, immutable. Generate unique filenames (e.g.,style.a1b2c3d4.css) when you deploy new versions. This allows you to setimmutableand a very longmax-agebecause the URL itself signifies a new version. - HTML Pages: These often contain dynamic content or link to assets that might have changed. Be more conservative.
Cache-Control: public, max-age=600(10 minutes) is common, or evenno-cacheif the content changes very frequently. - API Responses: Depends entirely on the data. If it’s user-specific and changes often,
private, max-age=0orno-cache. If it’s public, relatively static data (like a list of countries), you might usepublic, max-age=3600.
The true magic of ETag and Cache-Control lies in the 304 Not Modified response. When a browser uses If-None-Match or If-Modified-Since and the server responds with 304, it means the entire response body was skipped. The browser just uses its local copy. This saves significant bandwidth and reduces server load drastically for repeated requests.
The most misunderstood aspect is the interaction between immutable and max-age. When you set Cache-Control: public, max-age=31536000, immutable for a file like /static/style.a1b2c3d4.css, you’re telling the browser: "This file will never change. You can keep it for a year, and honestly, you probably don’t even need to ask me if it’s still valid until that year is up." This is a powerful signal that allows browsers to skip network checks altogether for the vast majority of the cache’s lifetime, leading to near-instantaneous loads on subsequent visits.
The next thing you’ll grapple with is how to generate those versioned filenames automatically during your build process.