Conditional Requests
The seat map is fetched 2,600 times a second during on-sale and it changes a few times a second. Almost every one of those fetches returns the same 60-kilobyte body the client already has. A response that carries an ETag lets the client ask "has it changed since this one?" and receive a 304 with no body when it has not. A request that carries If-Match lets a client say "update this only if it is still the version I saw," which is the HTTP form of the lost-update fix in Chapter 6. Both are the server's decision to offer, and Stagedoor offered neither until the spring on-sale sent 2,600 full seat maps a second across a link that could have carried 300.
The mechanism is small: one header on the response, one header on the request, one comparison in the middle. The design decision is where the validator comes from, and the whole topic turns on it. A validator computed by rendering the body saves bandwidth and nothing else. A validator read from a version column the service already keeps saves the render, the query behind it, and the bandwidth, and it is the same column the update check uses.
The ETag
An ETag is an opaque validator for one representation of one resource: a string the server can produce cheaply and compare exactly. A hash of the body is the obvious choice and the wrong one, because producing it means producing the body. The better validator is a version the service already has: seats.version, incremented on every write, or the event's updated_at, or for the seat map a counter that the cache layer of Chapter 9 bumps whenever any seat in the event changes. Stagedoor's seat map carries ETag: "8812-v2417", the event id and the map's version, and producing it is one Redis read.
The validator goes on every cacheable response, whether or not the client asked for it, because the client cannot ask for one it has never seen. A resource that has a version sends it on every read; what a client can do once it holds one is the rest of this topic.
If-None-Match and the 304
The client sends back the ETag it holds in If-None-Match. The service compares it to the current validator and, when they match, answers 304 Not Modified with the headers and no body. The client keeps using the copy it has. When they differ, the service answers 200 with the new body and the new validator, and the cycle continues from there. For the seat map that turns 2,600 full responses a second into 2,600 comparisons, each one a Redis read of the version counter, and a few hundred bodies for the clients whose copy is actually stale.
The comparison must not require building the body. That is the one design constraint, and it is why the validator is a version rather than a hash: a service that renders the seat map, hashes it, and then discovers the hash matches has done all the work the 304 was meant to avoid and saved only the bytes on the wire. Chapter 9 measures the hit ratio, the share of polls that end in a 304, and at on-sale it is above 90 percent.
If-Match and the Lost Update
An organizer has the same event open in two browser tabs. In the first she changes the price; in the second, opened an hour earlier, she fixes a typo in the title and saves. Without a precondition, the second save sends the whole representation with the old price, and the price change vanishes with no error anywhere. With one, the second tab's PUT /events/8812 carries If-Match: "v17", the validator it received an hour ago; the event is now at version 18; the service answers 412 Precondition Failed and the tab shows "someone changed this event; reload to see the current version."
PUT /events/8812 HTTP/1.1 If-Match: "v17" Content-Type: application/json {"title": "Late Show (corrected)", "price_cents": 4500, ...} HTTP/1.1 412 Precondition Failed ETag: "v18" Content-Type: application/problem+json {"type": "https://stagedoor.example/problems/stale-version", "title": "The event changed since you loaded it", "current_version": 18}
The exchange is the stale tab sending the whole event with the price it loaded an hour ago and the version it saw then. The service refuses because the event is at version 18, and the refusal carries the current validator and a Problem Details body naming it, so the client can reload and let the organizer redo the title fix on the current price. Behind the 412 is one line in the update statement, WHERE version = 17, and a check that the row count came back as one. Chapter 6 uses the same column, the same statement and the same zero-rows check to stop two buyers taking one seat; this is that fix, surfaced to the client as a status code.
Last-Modified and Its Limits
Last-Modified is a timestamp validator with one-second resolution, paired with If-Modified-Since and If-Unmodified-Since on the request. For a static asset that changes once a deploy it is fine. For a row that changes twice in one second it is wrong: two writes within the same second carry the same timestamp, a client holding the first sees "unchanged," and the seat it thinks is available was sold 400 milliseconds after its copy was made. Stagedoor sends it beside the ETag on the event list for the caches that only understand timestamps, and never instead of one, and the update path ignores it entirely.
Weak vs Strong
A validator prefixed with W/ is weak: it says the two representations are equivalent, not byte-identical. That is what a body that may be gzip-compressed by the edge needs, because the bytes the client received are not the bytes the service produced, and a strong validator on them is a claim the service cannot keep. A strong validator says the bytes are the same, and it is the only kind If-Match may accept, because an update precondition on "roughly the same" is no precondition at all.
Stagedoor draws the line like this: the seat map and the event list, which the balancer compresses, carry weak validators and serve 304s from them, since "equivalent" is all a 304 needs. The organizer-edited event, whose validator is the row version and whose bytes the service never transforms, carries a strong one, and If-Match on it is exact. Marking a validator strong on a body the edge may re-encode is how If-Match becomes unreliable in a way that shows up only under a proxy that was not there in staging.
Where It Pays
Public reads with high fan-out and any resource an organizer edits from a form. The event list and the seat map are the first: thousands of clients, one representation, a version that is cheap to read. The event, the seat prices and the organizer's settings are the second: few clients, real conflicts, a version column already there for Chapter 6. Everything else is not worth the header.
Two exclusions carry the weight. Never on a POST: there is no second read to save and no earlier copy to compare. And never one shared validator on a response that differs per user: the seat map with the buyer's own held seats highlighted is a different representation per buyer, and a CDN holding one copy under one validator serves one buyer's holds to the next. Either the validator includes the user, or the response says Vary: Authorization and Cache-Control: private so no shared cache keeps it, or the per-user part moves to a separate request. Chapter 9 chooses the third.
- Computing the
ETagby rendering the body — the 304 saved bandwidth and nothing else; the query and the render ran 2,600 times a second anyway, and the version column was there for free. - Ignoring
If-MatchonPUT— the service accepted both tabs, the second silently overwrote the first, and the organizer's price change vanished with no error in any log. - One
ETagfor a response that varies per user — the CDN serves one buyer's held seats to another; either the validator includes the user, orVary: AuthorizationwithCache-Control: private, or no shared caching at all. - A strong
ETagon a body the edge compresses — the bytes differ, the validator claims they do not, andIf-Matchbecomes unreliable behind exactly the proxy that was not in staging. Last-Modifiedas the only validator on a fast-changing row — two writes in one second share a timestamp, and a client holding the first is told it is current.- Sending the validator only when the client asks for one — the client cannot ask for a validator it has never seen, so the first response must carry it or no client ever gets to the 304.
- Derive every
ETagfrom a version the service already stores, and send it on every read of a resource that has one. - Honour
If-None-Matchwith a 304 on the hot public reads, and measure the hit ratio as a first-class metric of the seat-map path. - Require
If-MatchonPUTfor every organizer-edited resource, and return 412 with a Problem Details body naming the current version. - Mark validators weak on any body the edge may transform, and keep the strong one for the update path where the bytes are the service's own.
- Send
Cache-Control: privateandVary: Authorizationon any response that differs per user, before adding a validator to it.
Knowledge Check
A seat-map poll ends in a 304. What did the service save, and what did it not?
- The whole request, since the client's cache answers a 304 locally without contacting the service
- The body and the render behind it, but not the request itself, which still reached and ran on the instance
- The bytes on the wire only, since the service must always build the body to know whether it changed
- The database write, since a matching validator means no seat changed and the update is skipped
Two tabs edit the same event. How does If-Match stop the second save from erasing the first?
- The stale tab's version fails the WHERE clause, the update writes nothing, and the client gets a 412
- The service merges the two representations field by field and applies only the fields that differ
- The first tab's read locks the event row until its save, so the second tab's save waits and then fails
- The second save is queued behind the first with a 409 and applied automatically once the first commits
Why is Last-Modified the wrong validator for a seat row during on-sale?
- Because CDNs and browser caches ignore it and only act on an ETag, so the header is never consulted
- Because the timestamp is in the server's local timezone, so a client in another region compares it wrongly
- Because producing it requires a query for the row's latest write time on every poll, which the version avoids
- Because two writes inside the same second share a timestamp, so a stale copy compares as current
The seat map highlights the buyer's own held seats. What must change before it can carry a shared ETag?
- Mark the validator weak, so that caches treat the different per-buyer bodies as equivalent representations
- Send Cache-Control: no-store on the map, so no cache ever holds any version of it for any buyer
- Move the per-buyer highlight into its own request, or make the validator include the buyer's identity
- Shorten the cache lifetime to one second, so a leaked copy is replaced before another buyer sees it
You got correct