Don't re-send what hasn't changed
A client re-fetches a JSON resource — refreshing a page, polling for updates — and nine times out of ten, nothing has changed since the last fetch. Re-sending the full JSON body every time wastes bandwidth and server work for no reason. ETags and conditional requests are the HTTP mechanism for skipping that waste: the server tags each response with a fingerprint, the client remembers it, and on the next request the client asks "has this changed since fingerprint X?" — if not, the server replies with an empty 304 Not Modified instead of resending the JSON body at all.
The ETag: a fingerprint for a response
An ETag is an opaque string the server attaches to a response, meant to change if and only if the underlying resource changes:
GET /api/orders/ord_42200 OK
ETag: "a1b2c3d4e5f6"
Content-Type: application/json
{ "id": "ord_42", "status": "shipped", "total": 149.97 }The client stores this ETag alongside the response. On the *next* request for the same resource, it sends the ETag back in an If-None-Match header:
GET /api/orders/ord_42
If-None-Match: "a1b2c3d4e5f6"If the order hasn't changed, the server responds with no body at all:
304 Not Modified
ETag: "a1b2c3d4e5f6"The client already has the data from the last successful fetch and simply keeps using it — the round trip still happens (there's still a request), but the expensive part (serializing and transmitting the JSON body) is skipped entirely.
Where the ETag value actually comes from
An ETag can be computed however the server wants, as long as it changes when the resource does. Two common approaches:
// Content hash — robust, but requires computing a hash on every request
import { createHash } from "crypto";
const etag = '"' + createHash("sha256").update(JSON.stringify(order)).digest("hex").slice(0, 16) + '"';// Version or updated_at column — cheap, if your data already tracks it
const etag = '"' + order.id + "-" + order.updatedAt.getTime() + '"';The second pattern is far more common in practice: if your database row already has an updated_at timestamp or a version integer, deriving the ETag from it costs nothing extra, versus hashing the full serialized body on every single request.
Weak vs strong validators
An ETag prefixed with W/ is a weak validator — it asserts the response is *semantically equivalent*, not byte-for-byte identical (whitespace or key order might differ). A bare-quoted ETag is a strong validator, asserting byte-for-byte identity:
ETag: "a1b2c3d4" # strong — must be byte-identical
ETag: W/"a1b2c3d4" # weak — semantically equivalent is enoughFor JSON APIs, weak ETags are usually the right default — you rarely care that whitespace or key order is byte-identical, only that the *data* hasn't meaningfully changed, and weak validation is cheaper to compute correctly (a version number rather than a full-content hash).
If-Match: preventing lost updates
If-None-Match is for reads (skip re-sending unchanged data). Its counterpart, If-Match, guards writes against a classic race condition: two clients load the same resource, both edit it, and whoever saves second silently overwrites the first person's changes with no error. If-Match makes the write conditional on the resource being unchanged since you last read it:
PUT /api/orders/ord_42
If-Match: "a1b2c3d4e5f6"
Content-Type: application/json
{ "status": "cancelled", "total": 149.97 }If the current ETag no longer matches (someone else updated the order in the meantime), the server rejects the write with 412 Precondition Failed instead of silently applying a stale update over newer data:
412 Precondition FailedThis pattern — called optimistic concurrency control — is a far cheaper alternative to locking rows for the duration of an edit, and it's exactly the same idea as a database's optimistic-locking version column, expressed at the HTTP layer instead of the database layer.
Last-Modified: the simpler, coarser sibling
Before ETags, HTTP had (and still supports) Last-Modified/If-Modified-Since, based on a timestamp rather than an opaque fingerprint:
Last-Modified: Mon, 06 Jul 2026 09:15:00 GMTGET /api/orders/ord_42
If-Modified-Since: Mon, 06 Jul 2026 09:15:00 GMTIt's simpler to implement (many frameworks derive it automatically from a file's or row's modification time) but coarser — it only has one-second resolution, so two genuinely different updates within the same second are indistinguishable. ETags are preferred whenever precision matters or the "changed" definition isn't simply "the row's timestamp."
Implementing it in Express
app.get("/api/orders/:id", async (req, res) => {
const order = await getOrder(req.params.id);
const etag = 'W/"' + order.id + "-" + order.updatedAt.getTime() + '"';
if (req.headers["if-none-match"] === etag) {
return res.status(304).end(); // no body — client already has this
}
res.set("ETag", etag);
res.json(order);
});Most frameworks (Express with etag, Django REST Framework, ASP.NET) have built-in or easily-added middleware that automates the hashing and header comparison — you rarely need to hand-roll the comparison logic shown here in production.