skip to content

questions

3

Walk through how an HTTP ETag and the If-None-Match request header produce a 304 Not Modified response, and what a client is expected to do when it receives one.

level: juniorimportance: must knowfreq 68%

answer

  1. ETag opaque, changes iff representation changes
  2. stale -> If-None-Match -> 304 or 200
  3. 304 = headers only, no body
  4. 304 must refresh Cache-Control or you revalidate forever
  5. conditional GET uses weak comparison

basics

~20 s

The server sends an ETag identifying the representation. On the next request the client sends If-None-Match with that value; if it still matches, the server replies 304 Not Modified with headers but no body, and the client reuses its stored copy. Only the round trip is spent, not the payload.

solid answer

~50 s

On a first `GET` the server returns 200 with the body plus `ETag: "abc"` - an opaque token identifying that exact representation. The client stores body and tag together. When the cached copy is stale (its freshness lifetime has elapsed) the client does not simply refetch. It sends a **conditional request**: the same GET plus `If-None-Match: "abc"`. The server compares the tag with the resource's current one. If they match, nothing has changed, so it answers **304 Not Modified** - status line and headers only, no body - and the client marks its stored copy fresh again and serves it. If they differ, the server answers 200 with the new body and a new `ETag`. A 304 must carry the headers needed to update the stored entry, notably `ETag` and `Cache-Control`. `Last-Modified`/`If-Modified-Since` is the date-based equivalent of the same handshake; if a client sends both, `If-None-Match` takes precedence. The win is bandwidth and rendering: you still pay one round trip, but not the payload.

code

http · 16 lines
http
GET /style.css HTTP/1.1

HTTP/1.1 200 OK
ETag: "v7"
Cache-Control: max-age=600
Content-Length: 48213

...body...

--- 10 minutes later ---
GET /style.css HTTP/1.1
If-None-Match: "v7"

HTTP/1.1 304 Not Modified
ETag: "v7"
Cache-Control: max-age=600

go deeper

for a junior

Describe the two-step handshake and that 304 means reuse your copy, with no body sent.

for a middle

Add the freshness-then-validation split, the metadata a 304 must refresh, and weak comparison for conditional GET.

for a senior

Discuss cheap conditional handling at the origin, deterministic tag generation across a fleet, and the compression/Vary interaction.

for a principal

Position validation as the fallback layer beneath freshness policy, and weigh revalidation round trips against longer TTLs plus versioned URLs.

## Freshness versus validation HTTP caching has two phases. While a response is **fresh** - inside the lifetime given by `Cache-Control: max-age` or `Expires` - a cache serves it with no network traffic at all. Once it is **stale**, the cache does not throw it away. It asks the origin one cheap question: *is what I have still correct?* That question is a conditional request, and the token it carries is a **validator**. ## The entity tag An `ETag` is an opaque quoted string chosen by the server to identify one specific representation of a resource: ``` ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" ``` Opaque means the client must never parse it - it is a hash, a version number, an inode-and-mtime tuple, whatever the server likes. The only contract is: **the tag changes when the representation changes, and stays the same when it does not.** ## The handshake 1. `GET /style.css` -> `200 OK`, `ETag: "v7"`, `Cache-Control: max-age=600`, body. The client stores the body keyed with `"v7"`. 2. Within 600 seconds, reuse is silent - no request leaves the machine. 3. After 600 seconds the entry is stale. The client sends `GET /style.css` with `If-None-Match: "v7"`. 4. The server computes the current tag. Still `"v7"` -> `304 Not Modified`, no body. Now `"v9"` -> `200 OK` with the new body and `ETag: "v9"`. On a 304 the client **reuses its stored body** and updates the entry's metadata from the 304's headers - a new `Cache-Control` restarts the freshness clock, so the next 600 seconds are again request-free. This is why a 304 must include the headers a cache needs to refresh the entry; a 304 with no `Cache-Control` leaves the entry stale, and the client revalidates on every single use. ## Comparison rules For a conditional GET the comparison is **weak**: the tags need only be semantically equivalent, so `W/"v7"` matches `"v7"` and matches `W/"v7"`. That is deliberate - for "may I reuse my copy?" equivalent-enough is enough. (Write preconditions and range requests use strong comparison instead, where a weak tag never matches.) `If-None-Match: *` means "match if the resource exists at all" and is used to make a create-only PUT safe, not for caching. ## Why not just refetch Three reasons. Bandwidth: a 304 is a few hundred bytes against a payload that may be megabytes. Latency and rendering: reusing bytes already on disk avoids decode and layout work. Origin cost: the server can often answer a conditional request from a cheap version lookup without rendering the representation - though only if you implement it that way. A handler that fully builds the response body and then discards it to send a 304 saves the client's bandwidth but none of your CPU. ## Where it goes wrong Several practical failure modes are worth naming: - **The tag never changes** because it is derived from something that does not track content, so clients serve outdated data indefinitely. - **The tag always changes** - a timestamp folded in, or a load balancer sending each request to a node that hashes differently - so every revalidation is a full 200 and the mechanism buys nothing. Multi-node deployments must generate the tag deterministically from the content or a shared version, not from per-node state. - **Compression changes the bytes.** If a proxy gzips a response after the ETag was computed on the uncompressed body, the tag no longer identifies the transmitted representation. Servers typically distinguish encodings by suffixing the tag; combined with `Vary: Accept-Encoding`, this keeps variants apart. - **304 sent with a body**, which is a protocol violation - 304 never carries one. ## Relationship to Last-Modified `Last-Modified` plus `If-Modified-Since` is the older handshake with the same shape, comparing dates instead of tokens. Servers commonly send both validators; a client that has both should send both, and the server evaluates `If-None-Match` first, using the date only if no entity tag condition was supplied.

  • What headers must a 304 Not Modified response carry, and why?
    It must carry the metadata a cache needs to update its stored entry - the current ETag, and freshness information such as Cache-Control or Expires, plus Vary if it applies. Without a fresh lifetime the entry stays stale, so the client revalidates on every subsequent use and you lose most of the benefit. It must never carry a body.
  • Does a 304 save server work as well as bandwidth?
    Only if the handler is written to answer conditionally before generating the representation - looking up a version or hash and short-circuiting. Many implementations render the full response and then decide to send 304, which saves the client's bandwidth and rendering but none of the origin's CPU or database work. Making revalidation genuinely cheap is a deliberate design step.
  • A client sends both If-None-Match and If-Modified-Since. Which does the server evaluate?
    If-None-Match. Entity-tag conditions take precedence over date conditions, because the tag is the more precise validator and the date exists as a fallback for servers without entity tags. The server evaluates the date condition only when no entity-tag condition is present.

Like phoning ahead to ask whether the notice on the board still says what your photocopy says: if the answer is yes you keep reading your copy, and only the phone call cost you anything.

saying these in an interview costs you the question

  • Saying a 304 includes the body so the client can compare it
  • Thinking a conditional request is sent on every use rather than after the entry goes stale
  • Claiming a 304 always saves origin CPU regardless of how the handler is written
  • Generating an ETag per node from process-local state, so a load-balanced fleet never matches
  • Treating the ETag as parseable - reading a version number out of it client-side

context

open as a page

What is the difference between a strong HTTP ETag and a weak one written as W/"abc", and where does that distinction actually change server behaviour?

level: middleimportance: must knowfreq 45%

basics

~20 s

A strong ETag promises byte-for-byte identity of the representation; W/ marks a weak one promising only semantic equivalence. Conditional GET uses weak comparison, so both produce 304. Range requests and write preconditions like If-Match use strong comparison, where a weak tag never matches.

open as a page

An HTTP resource is served with only a Last-Modified header and clients revalidate with If-Modified-Since. What can go wrong, and how do you decide which validator to emit?

level: seniorimportance: should knowfreq 36%

basics

~20 s

HTTP dates resolve to one second, so a change in the same second a client fetched is invisible and the client keeps stale content indefinitely. Timestamps also move on deploys or restores without content changing. Emit an ETag derived from content or a version, keeping Last-Modified alongside it.

open as a page