skip to content

On an HTTP PUT or DELETE you can guard the write with either the If-Match header or the If-Unmodified-Since header. How do they differ, and when is each one safe to rely on?

level: middleimportance: should knowfreq 30%

answer

  1. ETag opaque, date has 1-second floor
  2. two writes in one second slip past the date
  3. both fail with 412
  4. If-Match wins when both are sent
  5. weak W/ tag never satisfies a write guard

basics

~20 s

If-Match compares an opaque ETag, so any change is detected exactly. If-Unmodified-Since compares an HTTP date with one-second resolution, so two edits inside the same second look unchanged and a stale write slips through. Prefer If-Match; use the date only when no ETag exists.

solid answer

~50 s

Both are preconditions evaluated before the write, and both yield **412 Precondition Failed** when they do not hold. The difference is the validator they compare. `If-Match` carries the `ETag` the client read. The server compares tokens, so any change that produced a new entity tag is caught, no matter how fast it happened. This is the strong guard, and it is what optimistic concurrency should use. `If-Unmodified-Since` carries a `Last-Modified` date. HTTP dates have one-second granularity, so if the resource is modified twice within the same second, or modified in the same second the client read it, the timestamp is unchanged and a stale write is accepted. Backends whose modification time is not updated on every change make it weaker still. So: use `If-Match` whenever the resource emits an ETag. Fall back to `If-Unmodified-Since` for legacy or static-file backends that only expose `Last-Modified`, and accept that it is best-effort. If a client sends both, the server evaluates `If-Match` and ignores the date.

code

http · 5 lines
http
09:00:00.10  B: GET /doc/7 -> Last-Modified: Tue, 12 Aug 2026 09:00:00 GMT
09:00:00.40  A: PUT /doc/7 (applied)  -> Last-Modified: Tue, 12 Aug 2026 09:00:00 GMT
09:00:00.80  B: PUT /doc/7
             If-Unmodified-Since: Tue, 12 Aug 2026 09:00:00 GMT
             -> 200 OK   (stale write accepted: timestamps are equal)

go deeper

for a junior

State that both guard a write and fail with 412, and that ETag comparison is exact while the date is only accurate to a second.

for a middle

Walk through the same-second scenario concretely and give the precedence rule when both headers appear.

for a senior

Argue for emitting an ETag on any resource worth guarding, and insist the check be atomic with the update.

for a principal

Frame validator choice as a contract decision - what precision the resource's write rate demands, and what you owe legacy clients that only understand Last-Modified.

## Two guards, one outcome A precondition header on a state-changing method tells the server: apply this only if my assumption about the resource still holds. Both guards are evaluated before any state changes, and both fail with **412 Precondition Failed**, leaving the resource untouched. What differs is the evidence. **`If-Match: "abc123"`** carries an entity tag - an opaque server-chosen token that identifies a specific representation. It is compared with the resource's current tag using strong comparison for writes: byte-identical tokens, and a weak tag (one prefixed `W/`) never satisfies a write precondition. Because the token is derived from the content or a version counter, any change at all produces a different token. **`If-Unmodified-Since: Tue, 12 Aug 2026 09:00:00 GMT`** carries a date, normally copied from the `Last-Modified` header of the client's earlier GET. The server compares it with the resource's current modification time and fails if the resource has changed since. ## Why the date version is weaker Four concrete problems: 1. **One-second granularity.** The HTTP-date format has no sub-second field. If a resource is written twice within the same second, the second writer sees an unchanged `Last-Modified` and its precondition passes even though it is working from stale content. On a resource with any real write rate, this is not theoretical. 2. **Same-second read.** If client B GETs at 09:00:00.2 and client A writes at 09:00:00.7, B's stored date equals the post-write date, so B's guard passes silently. 3. **Timestamp drift.** The date is generated by the server, so client clocks do not matter, but a cluster whose nodes disagree, or a storage layer whose modification time is set by a restore or a deploy, can move the timestamp in ways unrelated to content. 4. **Unreliable maintenance.** Many backends update a content hash on every change but only touch a modification timestamp on some paths. An ETag computed from the representation cannot drift like that. An entity tag has none of these problems because it is opaque and content-derived: the server may compute it from a hash of the body, a database row version, or a monotonic revision counter, and it changes exactly when the representation changes. ## Precedence when both are sent A request may legally carry both. The rule is that the entity-tag precondition wins: if `If-Match` is present, the server evaluates it and ignores `If-Unmodified-Since` entirely. The date is a fallback for servers that do not support entity tags, not a second opinion. Sending both is a reasonable belt-and-braces move for a client talking to an unknown server; it never makes the guard weaker. ## Choosing in practice Use `If-Match` for anything you actually care about: user-editable documents, configuration records, anything with concurrent writers. Emit a meaningful `ETag` on every representation so clients can. Use `If-Unmodified-Since` when you have no choice - a file-serving origin, an object store, or a legacy service that only exposes `Last-Modified`. Treat it as a substantial improvement over an unconditional write, not as a correctness guarantee, and do not build a workflow on it where a lost update is unacceptable. A useful design cue: if a resource is worth guarding, it is worth giving it an ETag. Adding a version column and rendering it as the entity tag is usually a small change and converts a probabilistic guard into a deterministic one. ## Server-side implementation Whichever guard you support, evaluate it atomically with the write. Reading the current ETag or modification time, returning the connection to the pool, and then updating reopens the very race the precondition exists to close. Fold it into the update statement (`... WHERE id = ? AND version = ?`, zero rows affected means 412) or run both inside one transaction.

  • A client sends both If-Match and If-Unmodified-Since on the same PUT. What does the server do?
    It evaluates If-Match and ignores If-Unmodified-Since. Entity-tag preconditions take precedence over date preconditions, because the tag is the more precise validator and the date exists as a fallback for servers without entity tags. Sending both is harmless and lets a client cope with either kind of server.
  • Can a weak ETag such as W/"v3" be used with If-Match?
    No. Write preconditions use strong comparison, and a weak tag never matches under strong comparison, so If-Match with a weak validator fails. Weak tags assert only semantic equivalence, which is enough to decide that a cached copy is good enough to reuse but not enough to prove you are overwriting exactly the version you read.

saying these in an interview costs you the question

  • Treating If-Unmodified-Since as equivalent protection to If-Match
  • Blaming client clock skew - the date comes from the server, the real problem is one-second resolution
  • Thinking the date precondition applies to GET; the conditional-GET counterpart is If-Modified-Since
  • Expecting a weak W/ ETag to satisfy If-Match
  • Evaluating the guard in one transaction and writing in another

context