Decide whether validation solves a real transfer problem

A partner app may request the same public product catalogue several times a day because it needs to know whether prices or descriptions changed. Sending an unchanged representation every time can waste transfer, especially when the response is large. An entity tag, or ETag, is a validator the server assigns to one selected representation. The client saves both that response and its tag. On a later GET it can send If-None-Match with the saved tag. If the current selected representation matches, the server can answer 304 Not Modified with no response body; otherwise it sends the current representation in a normal success response. This is a useful pattern only if clients actually retain the earlier body and can handle both outcomes.

Do not start by promising a percentage saving. First identify a stable, read-only endpoint where the same caller often asks for unchanged content. Record its response size, update frequency and client behaviour using actual observations if available. If the endpoint is tiny or changes on every request, implementing validators may add complexity with little gain. If a partner never stores the representation, a 304 would leave it with nothing to use. The business question is whether revalidation reduces avoidable transfer while preserving correctness, not whether the HTTP status looks sophisticated. Without measurements, treat expected savings as a hypothesis and test with a representative client.

Specify what a single URL selects. A catalogue endpoint may vary by language, currency, market, query filters or API version. The validator must refer to the representation selected by those request details. A tag for an English response must not license reuse of a Chinese response, and a tag for one currency must not validate another. If request headers drive selection, the Vary response header tells caches which request fields matter. If query parameters drive selection, include them consistently in the resource identity and test their combinations. A 304 confirms a stored response for the selected representation; it is not a general statement that the entire catalogue database has remained unchanged.

Implement the condition carefully

RFC 9110 defines ETag as an opaque quoted value. A server can produce it from a representation version or another method that changes whenever relevant output changes; clients must treat the value as opaque and send it back exactly. Strong tags indicate byte-equivalent representations under the standard's rules; weak tags, prefixed W/, permit semantic equivalence for cache validation. If the server cannot meet the strong validator requirements, it must mark the tag weak. Do not casually copy a database row timestamp into a tag if the response also depends on stock status, locale, merchandising or another data source. The tag has to describe what the endpoint actually returns.

The client sends the stored tag in If-None-Match on a later GET. Under RFC 9110, this precondition uses weak comparison, so a weak tag can validate a cached representation. If the condition evaluates false because a tag matches the current selected representation, GET or HEAD receives 304; if it evaluates true, the server continues to send the representation. If both If-None-Match and If-Modified-Since are present, the standard evaluates If-None-Match first and does not use the date condition for that case. This precedence matters when an old client supplies both. Do not confuse this read revalidation with If-Match on a state-changing request, which addresses a different lost-update problem and uses strong comparison.

A 304 response has no message body. RFC 9110 also requires it to include the applicable metadata that an equivalent 200 response would have sent, including fields such as ETag, Vary and Cache-Control when relevant. The client or intermediary can then update stored response metadata and use its existing body. Do not return 304 with an empty JSON object or turn a changed response into 304 merely because one database field stayed constant. On a changed representation, return the complete new body and its new tag. On a first request without a stored body, return the normal representation. These cases need explicit server and client tests because a superficially successful request can still display stale products.

An implementation review should ask how the server computes a tag when several services contribute to one response. Hashing the final serialised representation is one possible design, but it has processing cost and must account for content encoding and deterministic output. A stored revision counter is another possibility if every change that affects the response increments it reliably. The correct choice depends on the architecture; the invariant is that a tag must not claim equivalence when the client-visible selected representation has changed. Test changes at each contributing source, including a price feed or translated field, rather than changing only the primary product row.

Set a cache policy that matches the data

An ETag does not itself decide how long a response may be used without contacting the server. Cache-Control does that work. RFC 9111 says an unqualified no-cache response may be stored but must be validated successfully before reuse; no-store asks caches not to store the response. For a public catalogue that changes often, a policy requiring revalidation can be appropriate when the business wants clients to check before reuse. For content that may safely be reused for a limited time, a freshness lifetime can reduce requests further. Choose this deliberately with the product owner: a price or availability display may need a different rule from a stable category description.

Keep private or customer-specific data out of this public endpoint. A catalogue response that includes an individual discount, account eligibility or unpublished wholesale price is no longer the same public representation for everyone. That requires a separate privacy and caching design, not an extra ETag on the shared feed. Also define what happens when a product is removed: the old tag cannot make a deleted resource persist as though nothing happened. A cache may hold an earlier response, but the origin must answer the current resource state according to the API's contract. Revalidation is a consistency mechanism only when the underlying response identity and access controls are correct.

Consider intermediary behaviour. A partner client might call through a CDN, reverse proxy or its own gateway. Verify that the selected representation, ETag, Vary and Cache-Control survive each layer, and that a 304 is interpreted against the right stored 200 response. A proxy that strips Vary or reuses one language's body for another can turn an otherwise correct origin implementation into a customer error. Log enough to distinguish origin status, cache status and client-visible result without logging secret credentials. For a public endpoint, you can test through the same path partners use; testing only a local handler misses the very cache interactions that motivated the change.

Rehearse the changed and unchanged cases

Use a controlled example. Suppose a public endpoint returns the English catalogue for one market. The first GET returns a body and tag A. The client stores both. A second GET with If-None-Match A, while the selected representation is unchanged, should return 304 and no body; the client displays its stored body. The team then changes a product description that appears in the response. The next conditional GET should return the changed body and tag B. Changing only an internal field that cannot affect the response need not change the tag, if the chosen validator accurately reflects the representation. These are expected outcomes for a test fixture, not observed production timings or savings.

Expand the matrix before release. Request the same resource without a tag, with the current tag, with an obsolete tag, with a weak tag where supported, and with a tag from another language or query variant. Test a first-time client that has no stored body, a client whose stored body was evicted, and a proxy path. Confirm status, body presence, metadata and final displayed data for each case. When a client receives 304 but has lost its local body, it must recover by making an unconditional request rather than display nothing. This client recovery rule is an application decision, so write it into the partner integration guide and test it.

Release to one endpoint and a cooperating client first. Record actual 200 and 304 responses, the selected variant of the resource, unexpected 304s and client errors. A rising 304 share is not proof of better service if partners now see stale prices or retry repeatedly. Compare transferred bytes and customer-visible correctness under the same traffic mix before claiming an efficiency result. If monitoring is temporarily unavailable, correctness tests and partner feedback can still support a cautious release, but leave any quantitative performance claim unmade. The next practical step is to draw the endpoint's representation variants and test one unchanged-to-changed sequence through the production-like gateway.

Before you proceed

  • Does each ETag identify the selected response, including language and other variants?
  • Does a matching conditional GET return a body-free 304 with applicable cache metadata?
  • Can a client recover if it receives 304 after losing its stored response body?

Sources & further reading