An HTTP resource is served with only a Last-Modified header and clients revalidate with If-Modified-Since. What can go wrong, and how do you decide which validator to emit?
answer
- HTTP-date floor = 1 second
- same-second write -> permanent stale 304
- timestamp bumped by deploys and restores
- fleet nodes disagree; ETag from hash or row version
- send both; If-None-Match evaluated first
basics
~20 sHTTP dates resolve to one second, so a change in the same second a client fetched is invisible and the client keeps stale content indefinitely. Timestamps also move on deploys or restores without content changing. Emit an ETag derived from content or a version, keeping Last-Modified alongside it.
solid answer
~50 s`If-Modified-Since` compares a date whose format has **one-second resolution**. If a client fetches at 09:00:00.2 and the file is rewritten at 09:00:00.7, the recorded modification time is still 09:00:00, so the next conditional request gets a 304 and the client serves stale content **indefinitely** - the tie never breaks. That is the headline defect: not a transient window, but a permanently wrong cache entry. Secondary problems: modification time reflects storage, not content, so a redeploy, an rsync or a restore bumps it and forces full 200s for unchanged bytes; and behind multiple origin nodes the timestamps may disagree, so revalidation results depend on which node answers. An `ETag` avoids all of it because the server chooses the token: a content hash or a row version changes exactly when the representation changes and not otherwise. Emit an ETag as the primary validator, keep `Last-Modified` alongside as a fallback, and make the tag deterministic across nodes. When both conditions are sent, `If-None-Match` is evaluated and the date is ignored.
code
http · 4 lines09:00:00.20 GET /a.json -> 200 Last-Modified: Tue, 12 Aug 2026 09:00:00 GMT
09:00:00.70 a.json rewritten (mtime 09:00:00)
09:05:00 GET /a.json If-Modified-Since: Tue, 12 Aug 2026 09:00:00 GMT
-> 304 Not Modified <-- stale copy served, indefinitelygo deeper
Know that Last-Modified plus If-Modified-Since yields 304 and that its one-second resolution can miss a change.
Explain the same-second scenario concretely and why an ETag from content or version avoids it.
Cover deploy-induced timestamp churn, per-node divergence, and a concrete node-deterministic tag strategy, emitting both validators.
Set validator policy across the estate - content-hashed immutable assets where possible, version-derived ETags elsewhere, and an explicit stance on what the platform guarantees about staleness.
## The date-based handshake `Last-Modified` reports when the server believes the representation last changed: ``` Last-Modified: Tue, 12 Aug 2026 09:00:00 GMT ``` A client that stored this revalidates by sending `If-Modified-Since` with the same date. The server compares it with the current modification time: not newer means **304 Not Modified**, newer means a fresh 200 with the body. It is cheap, it needs no hashing, and static file servers get it for free from the filesystem. It is also the older mechanism, and its weaknesses are structural. ## Defect one: one-second resolution The HTTP-date format has no sub-second field. Every timestamp is truncated to a whole second. Now consider: ``` 09:00:00.20 client GET -> 200, Last-Modified: ...09:00:00 GMT 09:00:00.70 file rewritten, mtime becomes ...09:00:00 GMT 09:00:05 client GET, If-Modified-Since: ...09:00:00 GMT -> 304 Not Modified ``` The client holds the pre-09:00:00.70 bytes and will be told "not modified" **on every future revalidation**, because the recorded time never advances past what it already has. Unlike a race that resolves itself on the next event, this is a stable wrong answer that persists until some later write moves the timestamp into a new second. On a file that changes rarely after a burst of edits, that can be days. Careful origins mitigate by refusing to send `Last-Modified` when the modification time is within the current second, or by not producing a 304 in that case - but you cannot count on an arbitrary server doing so. ## Defect two: the timestamp does not track content Modification time is a property of storage, not of bytes. - A deploy that copies files, a container image rebuild, an rsync without `--times`, or a backup restore updates the modification time on files whose content is unchanged. Every client then does a full refetch of identical bytes - a bandwidth spike after every release. - Conversely, some content-generation paths update content without touching the timestamp, and clients stay stale. - Across multiple origin nodes, timestamps may differ by seconds. A revalidation load-balanced to node B, whose copy has a slightly later timestamp, returns 200 where node A would have returned 304. Hit rates become nondeterministic. ## Defect three: it says nothing about representation variants A single modification time describes the underlying file, not the negotiated representation. Two clients receiving gzip and identity encodings of the same file share one `Last-Modified`. An `ETag` can encode the variant (many servers suffix the tag per encoding), which combined with `Vary: Accept-Encoding` keeps the variants distinct. ## What an ETag fixes An entity tag is chosen by the server, so it can be made to change **exactly when the representation changes**: - a strong hash of the response body - precise, costs CPU, and needs the body; - a database row version or revision counter - cheap, changes on every write, and lets the handler answer a conditional request without rendering the body at all; - for static assets, a build-time content hash baked into the deployment. All three are deterministic across nodes if computed from shared state rather than local file metadata - which is the key operational requirement in a load-balanced fleet. ## Choosing what to emit A workable policy: 1. **Always emit an ETag** for anything you control. It is the only validator that supports conditional writes (`If-Match`) and range requests (`If-Range`) as well as conditional GET. 2. **Emit `Last-Modified` too** when you have a meaningful timestamp. It costs one header, gives older or simpler clients something to use, and is human-useful when debugging. Servers evaluate `If-None-Match` first and consult the date only when no entity-tag condition is present, so there is no ambiguity. 3. **Make the tag deterministic across nodes** - hash the content or read a shared version; never derive it from inode, per-process state, or local modification time. 4. **Weak or strong** follows from the resource: strong if it is written concurrently under `If-Match` or served in ranges, weak if its bytes wobble for meaningless reasons. 5. **For versioned static assets**, sidestep revalidation entirely: content-hashed URLs plus a long `max-age` with `immutable` means the conditional request never happens. The short form for an interview: `Last-Modified` is a legacy fallback with a one-second floor and storage-coupled semantics; `ETag` is the validator you design, and you should send both with the ETag as the real mechanism.
- Why is the one-second problem worse than an ordinary race condition?Because it does not resolve on the next attempt. The client's stored date equals the resource's recorded modification time, so every future If-Modified-Since produces another 304 and the stale copy is confirmed again and again. It only clears when some later write pushes the timestamp into a different second, which on a rarely-changed resource may be days away.
- How do you generate ETags that behave correctly behind a load balancer?Derive them from something every node computes identically - a hash of the response body, or a version column read from shared storage. Anything node-local, such as inode numbers or file modification times that differ between replicas, produces different tags per node, so revalidation outcomes depend on routing and hit rates collapse. Build-time content hashes are the simplest answer for static assets.
- If you emit both ETag and Last-Modified, does the client send both conditions, and which wins?A client holding both should send both If-None-Match and If-Modified-Since. The server evaluates the entity-tag condition and ignores the date, because entity-tag preconditions take precedence over date preconditions. Sending both is harmless and lets a client interoperate with servers that support only one of the two.
saying these in an interview costs you the question
- Blaming client clock skew, when the date is generated by the server and the real issue is one-second truncation
- Treating the same-second problem as a brief window rather than a persistently wrong cache entry
- Deriving ETags from file modification time or inode, reintroducing every weakness of Last-Modified
- Assuming a redeploy that rewrites identical files is cache-neutral
- Believing a server must pick one validator rather than emitting both