ETags, conditional requests and lost updates
If-None-Match saves bandwidth with 304, If-Match stops two clients overwriting each other with 412, and the strong and weak comparisons that decide both.
After this lesson you can
- Revalidate a cached response with If-None-Match and explain the 304
- Prevent lost updates with If-Match, 412 and 428
- Apply strong and weak ETag comparison correctly
An ETag is a validator: an opaque string the server attaches to a representation that changes whenever the representation changes — often a version number or a hash of the body.
HTTP/1.1 200 OK
ETag: "v42"
Cache-Control: no-cache
{"id": "7", "name": "Nino", "plan": "pro"}
A tag is quoted. Prefixed with W/ (W/"v42") it is a weak
validator, meaning "semantically equivalent" rather than "byte for byte
identical" — useful when, say, compression or whitespace may differ.
Revalidation: If-None-Match and 304
When a cached copy is stale (or marked no-cache), the client sends its
tag back and asks the server to send the body only if it changed:
GET /users/7 HTTP/1.1
If-None-Match: "v42"
If the current tag matches, the answer is 304 Not Modified with no body, and the client reuses what it has. The request still costs a round trip, but not the payload — and often not the database work, if the server can compute the tag cheaply.
Two comparisons
RFC 9110 defines two ways to compare tags:
- Strong comparison — both tags are strong and their opaque parts are identical.
- Weak comparison — the opaque parts are identical, whether or not either is weak.
If-None-Match uses weak comparison (a weak match is good enough to
reuse a cached copy). If-Match uses strong comparison, so a weak tag
can never satisfy it.
const isWeak = (t) => t.startsWith("W/");const opaque = (t) => (isWeak(t) ? t.slice(2) : t); const strongMatch = (a, b) => !isWeak(a) && !isWeak(b) && opaque(a) === opaque(b);const weakMatch = (a, b) => opaque(a) === opaque(b); export function run() { const pairs = [ ['"v1"', '"v1"'], ['W/"v1"', '"v1"'], ['W/"v1"', 'W/"v1"'], ['"v1"', '"v2"'], ]; return pairs.map(([a, b]) => `${a} vs ${b}: strong ${strongMatch(a, b)}, weak ${weakMatch(a, b)}`);}Lost updates, and If-Match
Two people open the same record, both at version "v42". The first saves
a change; the second saves theirs a minute later and silently overwrites
the first. Nobody gets an error. That is a lost update, and it is
exactly what conditional writes prevent — optimistic concurrency, with
no locks held between requests:
PUT /users/7 HTTP/1.1
If-Match: "v42"
The server performs the write only if the current tag still matches. If someone changed the resource in between, the condition is false and the answer is 412 Precondition Failed; the client re-reads, merges or asks the user, and tries again with the new tag.
function makeResource() { let version = 42; let doc = { name: "Nino", plan: "free" }; return { get: () => ({ etag: `"v${version}"`, doc: { ...doc } }), put(body, ifMatch) { if (ifMatch !== undefined && ifMatch !== `"v${version}"`) return 412; doc = body; version += 1; return 200; }, current: () => doc, };} export function run() { const results = {}; for (const conditional of [false, true]) { const r = makeResource(); const alice = r.get(); const bob = r.get(); const a = r.put({ ...alice.doc, plan: "pro" }, conditional ? alice.etag : undefined); const b = r.put({ ...bob.doc, name: "Nino K." }, conditional ? bob.etag : undefined); results[conditional ? "withIfMatch" : "plain"] = { statuses: [a, b], final: r.current() }; } return results;}Two more tools complete the picture:
If-None-Match: *on a PUT means "only if nothing exists here yet": a create that cannot overwrite.- 428 Precondition Required (RFC 6585) lets a server insist on a
conditional request, so a client that forgets
If-Matchis refused instead of risking a lost update.
When both headers are present, RFC 9110 evaluates If-Match first, then
If-None-Match.
Common mistakes
- Updating with a plain PUT and no precondition, so the last writer wins and earlier changes vanish without an error.
- Comparing a weak tag with
If-Match. Weak tags never pass strong comparison. - An ETag that does not change when the representation does (for example, derived only from the row's id), which makes every 304 a lie.
- Answering a failed
If-None-Matchon a PUT with 304. For GET and HEAD a falseIf-None-Matchis 304; for other methods it is 412.
What interviewers ask
- How do ETags reduce bandwidth? — the client sends
If-None-Matchwith its tag; a match returns 304 with no body. - Two users edit the same record. How do you stop one overwriting the
other? — optimistic concurrency: send
If-Matchwith the tag that was read; the server returns 412 if it changed; optionally require it with 428. - Strong versus weak ETags? — strong means byte-identical and is required for ; weak means equivalent and is enough for cache revalidation.
Exercise · Pro
Evaluate a conditional PUT
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade