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.

Revalidation with and without validators Comparison of what happens when a stale cached response is reused with validators present versus absent. Revalidation with and without validators Validators present • Conditional request with If-None-Match • 304 Not Modified, no body • ~300 bytes transferred • Server can skip rendering No validators • Unconditional request • 200 OK with full body • Full 40-200KB transferred • Server renders every time

Rapid Diagnosis

  • Check response headers on HTML and API responses for ETag and Last-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 get 304.
  • 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.

ETag vs Last-Modified Comparison of ETag and Last-Modified validators by precision, generation and common failure modes. ETag vs Last-Modified Aspect ETag Last-Modified Precision exact content identity one-second granularity Dynamic HTML hash of the body needs a meaningful date Multi-server consistency only if content-derived only if dates preserved Common failure per-server values changes on every deploy

Step-by-Step Resolution

1. Derive ETags from content, not from the filesystem

nginx
# 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

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

Repeat-visit HTML transfer by validator setup Bar chart of bytes transferred for a repeat view of an article page under different validator configurations. Repeat-visit HTML transfer by validator setup No validators 68KB Per-server ETag (LB) 51KB Content-derived ETag 0.4KB

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-Control and ETag on 304s.
  • Ignoring If-Modified-Since on dynamic routes. If you emit Last-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.