Apps Artificial Intelligence Cloud Computing CSS Cybersecurity Data Science Database Go JavaScript Linux Python Rust Software Engineering Web Development

HTTP Conditional Requests with ETag and Last-Modified

3 min read .
HTTP Conditional Requests with ETag and Last-Modified

HTTP caching is not only about choosing a long max-age. Applications often need clients to revalidate data because a resource can change, while still avoiding retransmitting the full representation when it has not changed.

HTTP validators solve that problem. The two common validators are ETag and Last-Modified.

Freshness and validation are different

A freshness directive can tell a cache that a response may be reused without contacting the server for a period:

Cache-Control: private, max-age=60

After the response becomes stale, a client can validate its existing copy instead of downloading the body again.

If the server previously sent:

ETag: "article-9f31"

then the client can send:

If-None-Match: "article-9f31"

If the current representation has the same validator, the server responds:

HTTP/1.1 304 Not Modified
ETag: "article-9f31"

A 304 response lets the client reuse its stored representation and avoids sending the normal response body.

Design ETags around representations

An ETag identifies a representation, not merely a database row.

If one resource can be returned in different languages, encodings, or projections, the validator must change when the relevant representation changes.

Common ETag sources include:

  • a content revision identifier;
  • a stable hash of the serialized representation;
  • a version column combined with representation variant.

Avoid hashing very large payloads on every request if a cheaper reliable revision identifier already exists.

Strong and weak validators

A strong ETag represents the stronger form of representation equality used by HTTP conditional logic.

A weak ETag is prefixed with W/:

ETag: W/"article-9f31"

Weak validators can be suitable for cache revalidation when representations are semantically equivalent despite byte-level differences. Operations that depend on exact representation identity can require stronger semantics.

Choose intentionally rather than adding W/ without understanding the comparison rules.

Last-Modified is simpler but less precise

A server can send:

Last-Modified: Tue, 01 Sep 2026 14:00:00 GMT

A client may later send:

If-Modified-Since: Tue, 01 Sep 2026 14:00:00 GMT

If the resource has not changed, the server can return 304 Not Modified.

Modification timestamps are easy to derive from stored records, but timestamp precision and update semantics can make them less precise than an explicit version identifier.

When both validators are present, follow HTTP’s defined precedence rules instead of inventing custom combinations.

Validators complement Cache-Control

A dynamic private resource might use:

Cache-Control: private, max-age=0, must-revalidate
ETag: "user-42-v18"

This permits storage in a private cache but requires validation before reuse.

A fingerprinted immutable asset has different needs because its URL changes whenever content changes:

Cache-Control: public, max-age=31536000, immutable

Use a caching strategy that matches the resource lifecycle.

Use Vary when headers change the representation

If request headers affect the response, shared caches need to know which ones matter.

Examples include:

Vary: Accept-Encoding

or:

Vary: Accept-Language

Without appropriate Vary metadata, a shared cache can reuse a representation for a request that should have received a different variant.

Avoid varying on high-cardinality headers unnecessarily because that fragments cache entries.

Conditional requests can protect writes

Validators are also useful for optimistic concurrency.

A client reads a resource with:

ETag: "profile-v7"

When updating it, the client can send:

If-Match: "profile-v7"

The server performs the modification only if the validator still matches. If another writer changed the resource first, the stale update can be rejected instead of overwriting newer data.

This is a different use from If-None-Match cache validation, but both rely on a stable version concept.

Common pitfalls

Generating a random ETag per request

A validator that changes when content does not makes every revalidation miss.

Reusing one ETag across different representations

If language, encoding, or projection changes the representation, cache metadata must distinguish those variants correctly.

Treating 304 as a normal response body

A 304 Not Modified tells the client to reuse its stored representation rather than receiving a new one.

Assuming validation removes all server work

The server still needs an efficient way to determine whether the resource changed. Choose a validator source that makes that check cheap.

Measure cache behavior

Useful metrics include:

  • percentage of requests carrying validators;
  • ratio of 304 to 200 responses;
  • bytes avoided;
  • revalidation latency;
  • cache hit and miss rates at each layer.

A high 304 rate can be useful, but if each validation still performs an expensive database query, the validation path may need further optimization.

Conclusion

ETag and Last-Modified let HTTP clients ask whether their stored copy is still valid instead of downloading it again blindly. Build validators from stable representation versions, combine them with deliberate Cache-Control rules, use Vary for negotiated content, and consider If-Match when write concurrency matters. Correct validators make caching efficient without requiring clients to tolerate stale data indefinitely.

Related Posts

chevron-up