You are designing a JSON API and must decide, for each resource, whether responses carry an ETag validator, a Last-Modified date, both, or neither. How do you make that call?
answer
- ETag = opaque version, any resolution
- Last-Modified = 1-second granularity
- validator must be cheaper than the body
- collections: max(updated_at) misses deletes
- validator must cover query params
basics
~20 sUse an ETag when you have a cheap exact version (row version, sequence, content hash) or when changes can happen within the same second. Use Last-Modified when a meaningful modification time already exists and human readability or heuristic freshness helps. Emit both when free; emit neither for tiny or per-request-volatile responses.
solid answer
~60 s**ETag** is an opaque version token the server defines however it likes — a row `version` column, a content hash, a collection sequence number. It has unlimited resolution, so two changes in the same second are distinguishable, and it works for resources with no natural "modified time", like a computed aggregate. **Last-Modified** is a date with one-second resolution. It is cheap when you already store `updated_at`, it is human-readable in logs, and it lets caches apply heuristic freshness when you send no explicit lifetime. Its weakness is exactly that resolution: sub-second edits are invisible, so a client can be served a stale copy. My default: single resources with a version or `updated_at` column get an ETag derived from it; collections get an ETag from `max(updated_at) + count` or a change sequence, because a member's deletion must change the collection's validator. Add `Last-Modified` when it exists anyway. Skip validators for responses that are volatile per request or so small the validator costs more than the body — and never compute an ETag by serializing the whole payload just to hash it.
code
http · 5 linesHTTP/1.1 200 OK
ETag: "v42-9c1f"
Last-Modified: Tue, 12 Aug 2026 09:14:02 GMT
Cache-Control: max-age=30
Content-Type: application/jsongo deeper
Know what each header is and that ETag is an opaque version token while Last-Modified is a date with one-second resolution.
Make the choice on resolution, cost of computing, and whether a genuine modification time exists; explain the collection problem.
Talk about deriving validators from version columns or change sequences so 304s cut origin work, and about validator stability as a serialization discipline.
Treat validators as a platform-wide contract decision — they enable both revalidation and optimistic concurrency — and set defaults teams follow rather than leaving it per endpoint.
## What the two validators are A **validator** is a token the server attaches to a representation so a later request can ask "has this changed?". HTTP defines two. - `ETag: "7c9e"` — an **entity tag**: an opaque string chosen by the server. Clients never interpret it, only echo it back in `If-None-Match`. Because it is opaque, you can derive it from anything: a database row version, a rowversion/xmin column, a monotonic change sequence, or a hash of the serialized body. - `Last-Modified: Tue, 12 Aug 2026 09:14:02 GMT` — the time the representation last changed, echoed back in `If-Modified-Since`. ## Choosing per resource **Resolution.** `Last-Modified` has one-second granularity, and HTTP dates cannot express anything finer. If a resource can change twice within the same second — counters, order status, anything edited by machines — a client that fetched mid-second can be told "not modified" when it was. An ETag has no such limit. This alone pushes most write-heavy API resources to ETags. **Cost of computing it.** The validator only pays off if it is far cheaper than the representation. A row `version` or `updated_at` column read by primary key is ideal: the handler can answer 304 after one indexed lookup. Hashing the serialized body is the opposite — you did all the work and saved only egress. If hashing is your only option, at least cache the hash next to the data. **Does a modification time exist and mean anything?** For a stored document, `updated_at` is natural. For a computed or aggregated representation (a dashboard rollup, a search result page) there is no honest modification time, and inventing one is worse than emitting an ETag over the inputs' version vector. **Collections are the hard case.** A list resource changes when any member changes, when a member is added, and when one is deleted. `max(updated_at)` alone misses deletions, so a common recipe is `max(updated_at)` combined with a row count, or better, a per-tenant change sequence bumped on every mutation. The validator must also cover the query parameters that shaped the response — page, sort, filter — or two different pages will share a validator. **Heuristic freshness.** RFC 9111 lets a cache guess a freshness lifetime when you send no explicit one, typically from the `Last-Modified` age. That is a reason to include `Last-Modified` on slow-changing public resources — but a better practice is to send an explicit freshness lifetime rather than rely on a heuristic you do not control. **Both.** Sending both is legal and common. Clients then send both `If-None-Match` and `If-Modified-Since`; a server that supports the entity tag evaluates that one and ignores the date. Do it when both are free; do not build a second validator pipeline just for completeness. **Neither.** Legitimate when the representation is different on every request by design (a live quote, a randomized feed), when it is smaller than the headers it would need, or when the resource is write-only or one-shot. Being explicit about "no validator" is a design decision, not an omission — document it so client teams do not build revalidation logic that never hits. ## Contract consequences beyond caching Entity tags are also the raw material for optimistic concurrency: a client that received `ETag: "7c9e"` can send `If-Match: "7c9e"` on its update so the server rejects the write if someone else changed the resource meanwhile. If you plan to offer that, you need ETags on exactly the resources clients mutate — and they must be strong ETags, since precondition evaluation on writes uses strong comparison. Deciding validators resource-by-resource is therefore not only a caching decision; it decides which resources can support safe concurrent editing. ## Stability is the whole game Whatever source you pick, the validator must be stable while nothing meaningful changed. Server timestamps in the body, request ids echoed into the payload, non-deterministic map ordering, and per-request pagination cursors all cause validator churn and turn revalidation into pure overhead — a round trip that always returns 200. Determinism in serialization is a prerequisite for conditional GET to be worth offering.
- How do you build a validator for a paginated collection endpoint?Derive it from something that changes on every mutation to the collection — a per-tenant change sequence, or max(updated_at) combined with a count so deletions are caught — and mix in the query parameters that shaped the response (page, page size, sort, filters). Otherwise two different pages can present the same validator and a client will be told 'not modified' for a page it has never seen.
- If a client sends both If-None-Match and If-Modified-Since, which wins?The entity tag. A server that supports ETags evaluates If-None-Match and ignores the date condition, because the tag is exact while the date is a one-second approximation. If-Modified-Since is only consulted when there is no usable entity tag.
saying these in an interview costs you the question
- Computing an ETag by serializing and hashing the full payload and calling it a saving
- Using Last-Modified for a resource that can change several times per second
- Deriving a collection's validator from max(updated_at) alone, so deletions go unnoticed
- Ignoring query parameters when building a validator for a filtered or paginated response
- Including a per-request timestamp in the body and then wondering why 304s never happen