ETag vs Last-Modified: Choosing and Fixing Cache Validators
This guide covers the revalidation half of HTTP Cache-Control Headers Explained, part of Advanced Caching Strategies & CDN Architecture. Every cached response eventually becomes stale. What happens next depends on validators. If the response carried an ETag or Last-Modified, the cache sends a conditional request (If-None-Match or If-Modified-Since), and the server can reply 304 Not Modified with no body — a few hundred bytes instead of the full response. Without validators, the cache must download everything again.
For HTML served with no-cache or short TTLs, validators decide whether repeat views cost a round trip or a round trip plus the full document. For CDNs revalidating against the origin, they decide whether the origin re-renders or merely confirms. They are easy to get subtly wrong: ETags that differ between servers behind a load balancer, weak ETags stripped by compression, Last-Modified dates that change on every deploy. Each mistake turns cheap 304s back into full 200 responses.
Rapid Diagnosis
- Check response headers on HTML and API responses for
ETagandLast-Modified. - Make the same request twice through different servers (or repeatedly through a load balancer) and compare ETags. Different values for identical content mean per-server ETags.
- Send a conditional request manually:
curl -sI -H 'If-None-Match: "<etag>"' <url>; you should get304. - Check reload behaviour in DevTools. Revalidated resources show status 304 and small transfer sizes.
Root Cause Analysis
1. Per-server ETags. Some servers derive ETags from inode numbers or modification times that differ across machines, so a revalidation that hits a different server never matches.
2. Compression changing ETags. Some servers or proxies modify ETags when compressing (or weaken them), so conditional requests for compressed responses do not match.
3. Deploy-time Last-Modified. Files copied during deploy get new modification times even when unchanged, invalidating every Last-Modified validator.
4. Dynamic HTML without validators. Frameworks rendering HTML per request often emit no validator at all.
Step-by-Step Resolution
1. Derive ETags from content, not from the filesystem
# nginx: etag is derived from mtime and size by default; make mtimes stable in
# your build artefact (e.g. set them to the commit time) so all servers agree.
etag on;
# trade-off: content-hash ETags computed by the app are more robust but cost a
# hash of the body per response; for large HTML that is still far cheaper than
# sending the body.
For application-rendered HTML, compute a hash of the rendered body (or of the data used to render it) and set it as a strong ETag.
2. Short-circuit rendering when the data has not changed
app.get('/articles/:slug', async (req, res) => {
const article = await getArticle(req.params.slug);
const etag = `"${article.id}-${article.updatedAt.getTime()}"`;
if (req.headers['if-none-match'] === etag) return res.status(304).end();
res.set({ ETag: etag, 'Cache-Control': 'no-cache' });
res.send(await render(article));
});
// trade-off: a data-derived ETag ignores template changes; include the deploy
// or template version in the ETag so a redesign invalidates old validators.
Expected outcome: revalidations skip both rendering and body transfer.
3. Preserve validators through compression and CDNs
Use servers and CDNs that keep strong ETags for compressed variants (or that treat weak ETags correctly), and confirm that the CDN forwards conditional requests to the origin on revalidation.
4. Stabilise Last-Modified across deploys
If you rely on Last-Modified for static files, preserve source modification times in the build (or derive them from git), so unchanged files keep the same date.
Verification
Use curl with If-None-Match against several backend servers (or many times through the load balancer) and confirm consistent 304s. In DevTools, reload a page and confirm HTML and API responses show 304. In server logs, the ratio of 304 to 200 responses for revalidated routes should rise.
Worked Example: Load-Balanced ETags
A news site ran six application servers behind a load balancer, each emitting ETags derived from in-memory render timestamps. CDN revalidations matched only one time in six, so the origin re-rendered nearly every revalidated article. Switching to ETags derived from the article's ID and update time plus the template version produced consistent values across servers. CDN revalidation 304 rate went from 17% to 94%, origin render volume fell by 70%, and TTFB p95 on cache-miss paths improved because the origin was less loaded.
Using Validators Alongside TTLs
Validators and TTLs work together rather than as alternatives. The TTL decides how long a cache may reuse a response without asking; the validator decides how cheaply it can confirm freshness when it does ask. A strong policy for HTML is a short or zero browser TTL plus a validator (cheap checks on every view), and a longer CDN TTL with tag purging plus the same validator (cheap origin revalidation when the CDN entry expires). For hashed assets, the TTL is a year and validators rarely matter. Think of validators as the safety net under every TTL: whenever a cache must check, they make the check cheap.
Common Mistakes
- Weak ETags where strong ones are needed. Range requests require strong validators; weak ETags (
W/"…") also may be ignored by some intermediaries. - ETags that include the server name. Breaks multi-server revalidation.
- Returning 304 without the headers the cache needs. Include updated
Cache-ControlandETagon 304s. - Ignoring
If-Modified-Sinceon dynamic routes. If you emitLast-Modified, honour it.
Edge Cases
Vary and validators. A response that varies (for example by Accept-Encoding) needs validators that distinguish variants or are the same across them consistently.
Personalised responses. ETags on personalised pages must not leak information; derive them from the response body, not user IDs.
Service workers. Workbox and similar tools can use validators for revalidation in stale-while-revalidate strategies.
HTTP/2 push and early hints. Unrelated to validators, but often configured in the same server block; keep them separate in reasoning.
FAQ
Should I send both ETag and Last-Modified?
It is harmless and maximises compatibility; when both are present, ETag takes precedence for caches that support it. If one of them is unreliable in your deployment (per-server ETags, deploy-time dates), send only the reliable one.
Do validators help hashed static assets?
Rarely, because immutable assets are never revalidated during their long TTL. They help only if users force a reload or the asset is evicted and refetched.
Is generating an ETag expensive?
Hashing a rendered HTML document costs far less than sending it. Data-derived ETags (ID plus update time plus template version) avoid rendering at all on a match, which is cheaper still.
How do CDNs use validators?
When a CDN entry expires, the CDN revalidates with the origin using the stored validator. A 304 lets the CDN refresh the entry's TTL without downloading the body — reducing origin bandwidth and render work.
What is the cost of a 304 for the user?
A round trip without a body. On mobile that is still 50–300ms of latency, which is why short TTLs or stale-while-revalidate can beat pure revalidation for content that changes rarely.
Related
- no-cache vs no-store vs max-age=0 — the directives that trigger revalidation.
- Stale-while-revalidate with nginx proxy_cache — revalidation at a reverse proxy.
- Diagnosing CDN cache misses from response headers — spotting revalidations in headers.