AlgoMaster Logo

HTTP Caching Semantics and Conditional Requests

High Priority14 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

When you return to a book's details page, the app may already have a copy of the data from your last visit. It could reuse those bytes, ask whether they are still current, or download a replacement. HTTP gives the server and client a shared way to make that decision.

This chapter explains response storage, freshness, validators, and conditional requests. It also covers the limits of stale reuse and invalidation, so a cache's behavior remains part of the API contract rather than an accidental optimization.

1. Storage and Reuse

An HTTP cache stores response messages for reuse. A private cache serves one user, while a shared cache can serve multiple users. Both need to answer two distinct questions: may the cache store this response, and may it satisfy this particular request now?

Storage depends on the method, status code, directives, and other protocol restrictions. Reuse also requires a matching target and representation. For example, a cached CSV response must not satisfy a request that accepts only JSON. Vary: Accept records that selection dependency.

The diagram shows a simplified cache decision for GET requests. It assumes the policy prohibits stale reuse and omits specialized cases such as range requests:

Having a stored copy is not enough. A cache can store a response that requires validation before every reuse. A stored response can also become stale while remaining useful as the basis for a conditional request.

Absence of Cache-Control does not universally prohibit caching. For eligible responses, a cache can estimate how long they remain fresh when no explicit freshness information is available. This estimate is a heuristic freshness lifetime. Caches can also store some error responses, including eligible 404 responses to GET. An API should state its policy explicitly when accidental retention would cause problems.

2. Freshness and Age

A response is fresh while its current age is less than its freshness lifetime. Freshness authorizes reuse under the applicable rules; it does not prove the underlying data has not changed.

Assume https://api.bookstore.example/books/book_1042/details returns public bibliographic data in JSON or CSV. The JSON representation is identical for every caller. A client requests it:

The origin server returns:

These examples use HTTP/1.1 over HTTPS. Each JSON body occupies one line without a trailing newline. Entity-tag values are illustrative identifiers the server assigns.

max-age=60 supplies a 60-second freshness lifetime. Date describes when the message originated; Last-Modified describes when the selected representation last changed. A representation can be old while the server has just generated the response carrying it.

When a cache serves a stored response without validation, it generates an Age field with its estimate of the response's current age in seconds. Age includes time in upstream caches, time in transit, and time in this cache; it is not simply time since the nearest cache received the response.

For an idealized timeline with negligible network delay and aligned clocks:

Scroll
TimeEstimated ageResult under this policy
14:00:000 secondsFresh
14:00:4040 secondsFresh, about 20 seconds remain
14:01:0060 secondsStale

Receiving the response with Age: 40 does not grant another full minute. A downstream cache continues accounting for the existing age. Real implementations also account for response delay and apparent age rather than relying only on this simplified clock subtraction.

If the book changes at 14:00:20, a cache can still serve its fresh copy at 14:00:40. Choosing the lifetime therefore means choosing how long clients may see an older version of this data.

3. Cache-Control Policies

Response directives express different storage and reuse constraints. The table uses their ordinary unqualified forms:

DirectiveEffect
publicAllows shared caching, subject to applicable rules
privateProhibits shared-cache storage, while permitting private caching
no-storeProhibits storing the response
no-cacheRequires successful validation before reuse
max-age=NSets the freshness lifetime in seconds
s-maxage=NSets the shared-cache freshness lifetime, overriding max-age there
must-revalidateProhibits stale reuse without successful validation

For example, Cache-Control: public, max-age=60, s-maxage=300 gives private caches a 60-second lifetime and shared caches a 300-second lifetime. The shared-cache directive also imposes revalidation requirements after that lifetime. Use it only if the data can tolerate that longer shared-cache window.

Expires supplies an absolute expiration time. max-age takes precedence over it, and shared caches give s-maxage precedence where applicable. Prefer a coherent policy rather than contradictory freshness values.

no-cache permits storage but requires validation even when the stored response would otherwise be fresh. must-revalidate permits fresh reuse and becomes restrictive once the response is stale. A response with max-age=0 is immediately stale; that alone is not the same as forbidding storage or all stale reuse.

Requests can also carry directives. A client sending Cache-Control: no-cache asks caches not to use a stored response without successful validation. It does not mean “delete every cached copy,” and validation can still avoid downloading the body again.

Choose storage policies with the data's audience in mind. For a private reading list, private, no-cache can allow a user's cache to retain a copy while requiring validation before reuse. If the service does not want HTTP cache storage at all, it can use no-store. Neither policy erases copies that other systems already hold or replaces authentication.

Shared caching of responses to requests containing Authorization has additional restrictions. Cookies, including Set-Cookie, do not automatically prohibit HTTP caching. Mark personalized responses deliberately, and do not rely on a cookie or validator alone to prevent cross-user reuse.

4. Validators

A validator is metadata a client or cache can send back to test whether its stored representation is still applicable. HTTP's common validators are ETag and Last-Modified.

An entity tag, which the ETag field carries, is an opaque quoted value such as "book-1042-json-v7". Clients store and return it without interpreting its internal structure. It need not be a hash or expose a database version.

A strong entity tag has no W/ prefix. It must change when the representation data changes. A weak tag, such as W/"book-1042-json-v7", can indicate semantic equivalence without guaranteeing byte-for-byte identity. For example, a server could consider differently formatted JSON equivalent for cache validation.

Validators belong to selected representations. JSON and CSV variants with different bytes must not share a strong tag. Compressed and uncompressed representations likewise need distinct strong tags when their representation data differs. A database row's version alone is insufficient if other inputs can change the returned content.

Last-Modified supplies an HTTP date for the selected representation's modification time. Because it records changes with one-second precision and relies on accurate modification times, it may distinguish versions less reliably than an entity tag. Two edits within the same second can be difficult to distinguish using a timestamp alone.

Validators do not establish freshness. A response can carry an ETag and still require validation on every use, or it can be fresh without carrying any validator. These fields describe how to check a copy, not how long to trust it without checking.

5. ETag Revalidation

At 14:01:10, the stored book response is stale. The client can send its entity tag in If-None-Match:

For GET, this asks for the representation only if its current tag does not match the supplied tag. If-None-Match uses weak comparison, so it can validate semantic equivalence even with weak tags.

Assume the representation is unchanged. The server responds:

A 304 response has no content. It tells the recipient to reuse the applicable stored representation and update its metadata using the validation response. Include relevant fields such as the current ETag, cache policy, and Vary information consistently. These examples omit Content-Length on 304; do not add a JSON body or use zero to describe the stored representation's length.

The exchange differs from an ordinary cache hit:

alt[Representation unchanged][Representation changed]Stored response is staleGET book detailsGET with If-None-Match304 with updated metadataRetain body and update metadata200 with stored body200 with new body and ETagStore replacement if caching rules allow200 with new bodyBookstore appHTTP cacheCatalog APIBookstore appHTTP cacheCatalog API
9 / 9
algomaster.io

Here, the app makes an ordinary GET and the cache performs validation on its behalf. The app receives a usable response body. A client manually managing conditional requests must handle the 304 and stored body itself; a tag without the corresponding content is not enough to reconstruct the representation.

Validation avoids transferring unchanged content, but it still involves a request to a validating server. It may also require application work to determine the current tag. It does not offer the same latency benefit as serving a fresh local response.

Now suppose an editor changes the title at 14:02:00. A conditional GET at 14:02:20 still carrying the old tag receives:

The client replaces both its content and validator. Keeping the old tag with the new body, or the new tag with the old body, breaks subsequent validation.

6. Date-Based Validation

If a stored response supplies a modification date, the client can validate it with If-Modified-Since:

Assuming the resource exists and remains unchanged at 14:03:30, the server can return:

For GET or HEAD, If-Modified-Since tests whether the selected representation changed after the supplied time. If the date is valid, the representation has not changed, and the request would otherwise succeed, the server returns 304. If the representation changed, the server sends the normal response with current content for GET.

When both If-None-Match and If-Modified-Since appear, the server ignores If-Modified-Since and evaluates the entity-tag condition. This prevents a less precise timestamp from overriding the tag check. The server ignores If-Modified-Since for methods other than GET or HEAD and ignores invalid date values.

Conditional requests do not bypass normal request checks. If an authenticated caller loses permission to a private list, a matching tag does not entitle the caller to a 304. The service applies its authorization policy. Likewise, a deleted book can produce 404; a conditional request does not require the server to preserve an old successful response forever.

7. Conditional Writes

Conditional requests can also guard state-changing operations. A precondition is a requirement that must hold before the server performs the requested action. These checks answer a different question from cache freshness.

Scroll
FieldBasic conditionTypical purpose
If-None-Match: "tag"No current tag matchesValidate a stored GET representation
If-Match: "tag"A current tag matches stronglyChange the version the client previously read
If-None-Match: *No current representation existsCreate without replacing an existing representation
If-Unmodified-Since: dateNo modification after the dateDate-based guard when an entity tag is unavailable

If-Match uses strong comparison, so a weak tag cannot satisfy it. When the client supplies both If-Match and If-Unmodified-Since, the server ignores the date condition in favor of the tag condition.

Suppose a reading-list owner obtained the strong tag "list-731-json-v4", but another edit has already produced version 5. The owner submits an otherwise valid replacement:

EXAMPLE_TOKEN is a nonfunctional credential placeholder. Assume the caller still has edit permission. The server rejects the stale precondition:

The check and write must act as one guarded operation so a competing edit cannot slip between them:

PUT with If-Match version 4Replace only if current version is 4Current version is 5, no write412 Precondition FailedList editorReading-list APIList storageList editorReading-list APIList storage
4 / 4
algomaster.io

The client needs to retrieve the current state and review the intended edit against it rather than blindly repeat the same stale edit. An invalid settings value would be a separate validation problem, and missing permission would remain an authorization problem.

The key status distinction is that a matching If-None-Match on GET or HEAD yields 304, while a failed write precondition normally yields 412. An ETag's presence does not itself require all writes to include a precondition; that requirement belongs to the API contract.

8. Stale Responses and Invalidation

Stale content is not automatically unusable. An explicit policy can permit limited reuse while validation occurs or when the origin fails. For example, the response directive stale-while-revalidate=30 permits stale reuse for up to 30 seconds beyond the freshness lifetime while revalidation happens in the background. stale-if-error=120 permits limited stale reuse when qualifying errors prevent obtaining a usable response.

These are permissions, not guarantees of background refresh or successful recovery. Other directives can prohibit stale reuse. In particular, must-revalidate forbids serving a stale response without successful validation, including during disconnection. If the cache must validate a response but cannot complete the check, it must report failure rather than silently promise current data.

Choose stale permissions according to the data. An older author biography may be acceptable during an outage. A cached availability response does not authorize a purchase; checkout still needs to enforce current business rules.

Invalidation makes a stored response unavailable for ordinary reuse without obtaining or validating current state. When a cache forwards an unsafe request, such as PUT or DELETE, and receives a non-error response, it must invalidate stored responses for the target URI. That rule affects caches that observe the exchange; it is not a broadcast to every browser and intermediary.

Related URLs are another limit. Updating a book's details does not automatically invalidate every search result or catalog page containing that title. HTTP caches do not know those application dependencies. Likewise, adding no-store to future responses cannot recall an older fresh response from a cache that has not contacted the service again.

Treat freshness lifetimes, validation, and invalidation as complementary controls. A short lifetime bounds ordinary fresh reuse, validators make checking cheaper, and application-aware invalidation can address relationships that the protocol cannot infer.

Summary

HTTP caching separates permission to store a response from permission to reuse it. Freshness uses response age and policy, while validators let a client check an existing representation without downloading unchanged content.

ETag and date conditions can produce 304 for an unchanged read, a new 200 representation when content changes, or 412 when a write precondition fails. Correct behavior also depends on variant matching, permissions, metadata updates, and explicit stale-response rules. No individual header replaces the complete contract.