An ETag is an opaque fingerprint of a resource's current representation, and it does two separate jobs that get conflated: it lets a client avoid re-downloading a resource that hasn't changed, and it lets a client avoid overwriting a resource that has changed since it last read it. Both jobs use the same header, generated the same way, but they're checked with different conditional request headers and they solve different bugs — wasted bandwidth in one case, lost updates in the other.
Generating an ETag that means something
An ETag can be a hash of the resource body, a version counter stored in the row, or a last-modified timestamp encoded into a string — the client shouldn't need to know or care which, since the value is opaque by design. A version counter (incremented on every write) is usually the cheapest and most reliable option: it's already something a well-designed table has for optimistic concurrency, it's cheap to compare, and unlike a content hash it doesn't require re-serializing the whole resource just to check if it changed.
GET /v1/invoices/4471 HTTP/1.1
If-None-Match: "v7"
HTTP/1.1 304 Not Modified
ETag: "v7"
Cache-Control: private, max-age=60
If-None-Match: cheap reads when nothing changed
A client that already has a cached copy of a resource sends If-None-Match: "v7" on its next GET. If the server's current ETag still matches, it returns 304 Not Modified with an empty body — no re-serialization, no re-transfer, just a cheap comparison and a tiny response. This is the same mechanism Last-Modified/If-Modified-Since provides, but ETag comparison is exact where timestamp comparison is not: two writes within the same second produce identical Last-Modified values but different ETags if the content actually differs.
If-Match: preventing lost updates on writes
The more consequential use of ETags is on writes. A client that fetched a resource, edited it locally, and now wants to save sends If-Match: "v7" on its PUT or PATCH. If another client updated the resource in between — the server's current ETag is now "v8" — the precondition fails and the server returns 412 Precondition Failed instead of silently overwriting the other client's change. This is optimistic concurrency control implemented entirely through standard HTTP headers, with no bespoke versioning API required.
PATCH /v1/invoices/4471 HTTP/1.1
If-Match: "v7"
Content-Type: application/json
{"status": "paid"}
HTTP/1.1 412 Precondition Failed
ETag: "v8"
A 412 response tells the client its view of the resource is stale — the fix is to re-fetch the current representation, reconcile the conflicting edit (automatically or by prompting the user), and retry with the new ETag. Treating 412 the same as 400 in client error handling throws away exactly the information that makes ETags worth implementing.
Strong vs. weak ETags
A strong ETag ("v7") asserts byte-for-byte identical content; a weak ETag (W/"v7") asserts semantic equivalence — useful when a server wants to consider two responses equivalent despite trivial differences like whitespace or field ordering. Weak ETags are the more forgiving default for If-None-Match cache validation; strong ETags are required if you're using ETags for range requests or byte-exact validation. Most REST APIs only need weak ETags and can skip the distinction entirely by documenting that all their ETags are weak.
An ETag derived from a timestamp column that gets touched on every read (a "last accessed" field, say) will never match on a second request even when nothing meaningful changed, defeating the entire point. Base ETags only on fields that represent the resource's actual state.
Wrapping up
ETags solve two problems with one mechanism: If-None-Match avoids re-transferring unchanged reads, and If-Match prevents one client from silently clobbering another's write. Both are standard HTTP, both are cheap to implement against a version counter you probably already have, and both turn "hope nobody edited this at the same time" into an explicit, checkable precondition.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.