skip to content

Conditional GET as API Design

Designing validators into your API so clients can poll cheaply: choosing ETag vs Last-Modified per resource and returning 304 instead of full bodies. Interviewers ask because it's the standard answer to 'clients hammer this endpoint every second — what do you do?'.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

4

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?

level: middleimportance: must knowfreq 52%

answer

  1. ETag = opaque version, any resolution
  2. Last-Modified = 1-second granularity
  3. validator must be cheaper than the body
  4. collections: max(updated_at) misses deletes
  5. validator must cover query params

basics

~20 s

Use 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 lines
http
HTTP/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/json

go deeper

for a junior

Know what each header is and that ETag is an opaque version token while Last-Modified is a date with one-second resolution.

for a middle

Make the choice on resolution, cost of computing, and whether a genuine modification time exists; explain the collection problem.

for a senior

Talk about deriving validators from version columns or change sequences so 304s cut origin work, and about validator stability as a serialization discipline.

for a principal

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

context

open as a page

You are designing a JSON REST API and want conditional GET support so repeat readers can be answered with HTTP 304 Not Modified. What does the server have to implement for those 304s to actually be cheap, where do the savings land, and when would you decide the feature is not worth adding at all?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A 304 pays off only if deciding "unchanged" is far cheaper than building the response — derive the validator from a stored version column or a hash saved at write time, and check it before rendering. Savings are egress bytes and client parsing; origin CPU only if you short-circuit.

open as a page

A public API has clients that poll an endpoint every minute looking for changes. How would you design that endpoint and its rate-limit policy so polls that find nothing new are cheap for both sides?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Give the endpoint a stable, cheaply computed ETag, require clients to poll with If-None-Match, answer unchanged polls with 304, and do not charge those 304s against the rate limit. Publish the polling interval, and offer webhooks or a change-feed for clients that need lower latency.

open as a page

HTTP entity tags come in a strong form like "abc" and a weak form written W/"abc". As an API designer, when would you deliberately issue a weak ETag, and what do you give up by doing so?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

A strong ETag promises byte-for-byte identical content; a weak one promises only semantic equivalence. Issue weak tags when the bytes may vary harmlessly (compression, field ordering, cosmetic fields) but the meaning does not. You give up range requests and optimistic-concurrency preconditions, which need strong comparison.

open as a page