When the browser reopens the article, it receives 304 Not Modified, and network round trips still occur. This saves the time it takes to transmit the main content, but not all delays. To decide whether to use long caching or revalidate, you must distinguish between “the object is still fresh” and “the server confirms that the object has not changed.”
This document assumes that GET resources are publicly accessible, and that the client supports HTTP caching. While optimistic concurrency for write operations can also leverage entity tags, it cannot simply apply the If-None-Match handling used during reads.
Version the response representation, not the filename
HTTP/1.1 200 OK
ETag: "article-v7"
Cache-Control: public, max-age=0, must-revalidate
Content-Type: text/html; charset=utf-8
...
The next request can include If-None-Match: "article-v7". If the selected representation is unchanged, the server returns 304 so the client can reuse its local body. The entity tag must track the actual response, not merely the Markdown filename or modification date. Layout changes, navigation updates, and converter upgrades can all change the final HTML.
Content hashing is a viable strategy, but define whether it hashes the final representation or an intermediate artifact. If the deployment system already provides entity tags, applications should not arbitrarily replace them. Strong tags require byte-level identity; weak tags express semantic equivalence with the W/ prefix. Their applicable comparison rules differ too.
First, the appropriate representation is selected based on the request: this includes the path, language, content encoding, and other valid variations, which collectively determine the current resource. Then, the validator for this representation is calculated or read, and the conditions in the header are compared according to the specifications. Finally, a 200 or 304 response is generated. If a common tag is first compared across the entire site, and then the language is determined, this could allow the Chinese version to be used for an English request. The validator cannot replace the correct cache key; it operates after the representation has been selected.
Second, distinguish object freshness from content version. During a long freshness period, the browser can reuse a response without contacting the server, even if the server’s ETag has changed. The client sends conditions when validation is needed. If urgent updates must become visible, a fixed article URL cannot have an unacceptably long freshness period. The server cannot directly order an offline client to delete its copy either. Design public content with the possibility of long-term retention in mind.
Finally, consider the source of the response. The browser sends a conditional request, and the CDN may directly verify and return the result, while the origin server does not execute it at all; or the CDN may re-verify with the origin server. Seeing “304” does not necessarily mean that the origin server’s load has decreased. To determine where the savings occur, you need to record the origin server’s request volume, edge hits, and actual transmitted bytes. If the performance goal is to reduce network round trips, you should optimize the freshness or resource identity, rather than simply increasing the proportion of conditional requests.
Don’t hand-write a parser that only supports single-valued expressions.
The condition header may contain multiple tags, or it may be *. A simple string comparison only covers the most basic cases. Compression, range requests, and weak tag comparisons further increase complexity. Prioritize using established HTTP server or platform implementations; if customizing, ensure proper parsing and testing according to specifications.
The HEAD request should return metadata that matches the GET request, but without a body. A 304 response should also not have a body, but it should retain any headers required for protocol-level or caching updates. If the status code is correct but key caching headers are missing, the intermediate layer will be unable to correctly update the object.
The two HTML snippets have different whitespace, but the same meaning. Weak tags can express this equivalence. Strong tags, on the other hand, must satisfy the byte consistency constraint. Weak tags are not “inferior versions” of strong tags; they express different guarantees. The requirements for validation and range concatenation differ for these guarantees. When choosing, you should consider the intended use, rather than arbitrarily removing prefixes for easier comparison.
Compression makes the representation problem more apparent. The same plaintext, when encoded differently, results in different byte sequences. The platform may generate different strong tags, rewrite them as weak tags, or remove them altogether. The specific behavior must be verified experimentally. If an application uses the hash of the uncompressed plaintext as a strong tag, but then changes the compressed bytes, the claim may not hold. Avoid having edge conversion, content compression, and application tagging controlled by separate, unrelated components.
Range downloads require particular caution. A client might request the first half of an older resource and then request the second half of a new resource, potentially resulting in a corrupted file. The If-Range header is used to specify that the range transmission should continue only if the condition is met, otherwise, the complete resource should be retrieved. However, it has its own validation rules. If the current requirement is simply for a technical article, there’s no need to implement a full range solution manually; handing it over to a mature server can avoid turning read optimization into a complex protocol engineering project.
Fresh Cache and Validated Cache Each Have Their Own Costs
JavaScript or CSS with content-hashed URLs can use a long max-age and the immutable directive because new content receives a new URL. HTML generally needs more timely updates and can use a shorter freshness period with validation. You cannot give fixed-URL HTML an indefinitely long cache lifetime and expect changing its ETag to take effect immediately. A client need not ask the server while its cached response is fresh.
no-cache does not mean “prohibited from storing,” but rather requires verification before reuse. no-store is the one that truly prohibits storage. Mixing the two together can lead to residual private data or the repeated transmission of publicly available content. Cache control should be determined based on the sensitivity of the content and the update requirements, rather than simply copying a fixed header.
Don’t Treat Modification Time as the Ultimate Content Identity
“Last-Modified” can be a useful validator for some resources, but the precision of the timestamp and the source of updates need to be clearly defined. If multiple dependencies change at the same second, or if the deployment retains the source file’s timestamp or the generator reverts to an older timestamp, the final content may change while the timestamp remains the same. When both “ETag” and “Last-Modified” are present, there is a defined priority relationship between them, and the protocol should handle this. It is not possible to arbitrarily choose one and return “unmodified” without the other.
Static builds can generate reliable labels for the final product, avoiding the need to recalculate on every request. Dynamic composition, on the other hand, requires evaluating the computational cost. If, for example, the entire database query and rendering are performed solely to determine if something is unchanged, then 304 only reduces the output bytes without reducing the backend workload. While it’s possible to maintain a reliable version in the data layer, this version must change whenever any of the output dependencies change. In the absence of a reliable version, it’s better to return a simple “200 OK” status code rather than generating a potentially collision-prone “approximate” label.
Clients may also send very old tags, and the server should not assume that they originate from the most recent deployment. Tag comparison is primarily focused on the current resources, and it is not necessary to retain all historical content to determine if they are different. When resources are deleted or permissions are changed, resource and permission checks should be performed first, and then verification should be considered. It is not permissible to continue reusing restricted content simply because a tag was previously valid.
This is also the reason for testing identity changes: If the same URL returns different responses for an anonymous user, User A, and User B, the public caching policy should first be isolated, and then the tags should be used to express each version separately. Otherwise, a coincident tag could lead the client to receive an incorrect validation conclusion. Conditional request optimization must be based on correct resource selection and authorization, and cannot be used to circumvent business logic by targeting tags.
The Markdown content remains unchanged, but the page header navigation has been updated, resulting in a modified HTML file. Relying solely on the modification time of the source file as a validator could cause the browser to continue displaying the old navigation. If the content hash is calculated based on the final output, it can cover these changes. However, it’s important to clearly define what the hash represents, especially when dealing with compression or transformation. For large, dynamic pages, recalculating the hash on each re-render can consume more resources than simply transferring the data.
Another approach is to combine reliable versions: the article version, the layout version, and the public data version together to form a version representation. This avoids reading all bytes each time, but requires that any dependencies affecting the output also be part of the version change. Timestamps are only suitable as part of the criteria if the accuracy, time zone, and update process are reliable, and it should not be assumed that the file system modification time is always unique. A second update within the same second is a clear counterexample.
Error handling matters too. If validator generation fails, fall back to a normal 200 response rather than inventing a fixed tag. A fixed tag could make different content perpetually appear unchanged. A failed cache optimization must not cause reuse of the wrong body. Decide storage for sensitive resources according to privacy policy; having an ETag does not automatically permit public caching.
Conditional requests can also prevent overwriting.
Assume both editors have read version 7. The first editor saves, resulting in version 8. The second editor, without any conditional saving, overwrites the first editor’s changes. The API can be designed to require an If-Match header to match the current version. If the header doesn’t match, the API returns a failure, prompting the client to refresh and merge. This is different from the If-None-Match approach used during reading, where matching allows for reuse. In the writing scenario, matching is required to proceed.
The server must keep version checking and updating atomic. Reading a database version, comparing it in application code, and then issuing an ordinary update still allows two concurrent requests to pass. Use a version-conditional update or an appropriate transaction mechanism, and determine success from the affected-row count. An ETag carries a version at the protocol layer; it does not itself provide database concurrency control.
Regarding resource creation, conditional headers can express the requirement to create only when the target does not exist, while still adhering to specific interfaces and specifications. The error response should inform the user that they need to refresh or handle the conflict, and should not automatically retry or overwrite. This example illustrates that conditional requests not only save bandwidth but also express state prerequisites. The correctness of the prerequisite validation depends on backend atomic operations, rather than the presence or absence of a specific header.
Establishing the Correct Order of Preconditions for a Resource Lifecycle Check
Suppose a client caches version 7 of an article that is subsequently withdrawn. When it requests the article again with the version-7 tag, the server must first establish that the resource still exists and access is allowed. It must not return “not modified” merely because it recognizes the tag. A 304 permits reuse of the existing body, so the wrong order can fail to reflect deletion or permission changes. Resource state, representation selection, and condition evaluation are distinct stages, not one cache-hit branch.
Consider resource migration again. The old path is now redirected to the new path, and the client sends the old path with the old content’s tag. The server should not treat the historical content validator as the identity represented by the current redirection. The actual protocol implementation should handle the current response semantics and check the validity of the cached redirection. The old content address and the new content address both point to the same article in the application layer, which does not mean that all HTTP metadata can be used as is.
For a permission-change test, first obtain a response using an authorized identity, then change server-side permissions and reuse the old condition header. Assert that the result reflects current authorization, not an old tag match. When a public article becomes private, examine shared-cache policy and existing copies too. You cannot guarantee that clients will forget text they already downloaded. Conditional requests constrain the server’s current response; they cannot retract historical copies.
Finally, test a content change between validator computation and response delivery. Dynamic systems must ensure the body and tag come from the same read version. Reading a version number and then independently reading the body can attach a version-7 tag to version-8 text. Use a consistent data snapshot or a completed immutable artifact. Static deployment makes it easier to generate tags and bytes together; runtime composition needs an explicit read-consistency boundary.
Ensuring Conflict Visibility When Pre-Conditions Are Not Met
After editor B’s stale-version save fails, the client must not automatically fetch a new tag and resubmit the old body. Doing so would bypass concurrency protection. Preserve B’s unsaved input, show the current server version, and let the user compare or merge. Conflict responses must not expose fields the requester lacks permission to view. Perform version checking after authorization; a failed precondition must not become a way to query private state.
The results of database condition updates should be differentiated based on whether the record does not exist, the current user lacks permission to modify it, or the version is mismatched. The external representation of these results should be determined by the interface’s disclosure protection strategy. Internal diagnostics can retain categorization, and the client may not necessarily need to see all the details. After a failed update, the system should not record a successful event or send notifications. Successful writes should only be confirmed after a transaction is committed. If external notifications cannot be completed atomically with the transaction, a clear subsequent delivery mechanism should be used, rather than treating tag comparison as a guarantee for the entire business process.
Repeated requests raise a different issue. A write based on version 7 may succeed but lose its response; retrying the same write then fails the old-version condition. That does not prove the first attempt failed. Version preconditions prevent overwriting, while idempotency identifiers recognize the same operation. A recovery interface can query the operation result or read the current state. One precondition failure does not establish whether an external side effect needs to run again.
Conditions specified in the interface also need to be consistent with the resource identity. If the path is provided by the client, first verify that the target and permissions are valid; if the resource ID is generated by the server, whether conditional creation is supported depends on the specific contract. “Target not found” should not be misinterpreted as “the same business object does not exist.” The database may still require business-level uniqueness constraints. HTTP conditions provide the prerequisites for the request, and cross-resource business invariants are still maintained by the backend.
Conducting Two Control Experiments to Establish Baseline Costs for Validators
For the first experiment, use already-built static HTML and compare full 200 responses, 304 responses, and fresh-cache reuse. Measure body transfer, request counts, and round-trip time for each path. For the second, use dynamically generated responses of the same body size and observe whether validation still performs database queries and rendering. These experiments answer different questions: the first examines the transfer path, the second backend work. Savings on static assets do not establish that dynamic endpoints also reduce computation.
The labels used in the experiment must come from actual responses, and should not be fixed to “article-v7”. Record whether CDN hits occurred, whether compression and browser caching were enabled, and avoid attributing platform-level optimizations solely to application-specific conditions. Test with both unchanged and changed inputs, where changes include not only the content but also the layout and language. Report all failures and timeouts, and do not compare only the average of successful requests after removing failed samples.
Content hashing also has costs. If the entire large file is read and hashed every time a request is made, the saved network bandwidth could be offset by additional CPU and memory usage. Pre-generated tags can shift the cost to the build phase. Dynamic combination versions can reduce computation, but rely on all input sources to reliably participate in version updates. These two implementations can be recorded separately as byte validation and dependency version validation, allowing maintainers to know where to make changes when new navigation data is added.
Check the client’s actual cache update after a 304
A 304 is not a new blank page. It tells a cache to reuse the existing body and update applicable metadata. Begin acceptance testing with a 200 response to establish a cached object. Then send a conditional request and observe whether the cache retains the correct body and updates headers. Sending conditions from a tool without a cached body checks the server’s status code but cannot simulate a reader’s actual reuse. Command-line requests and browser experiments therefore complement rather than replace each other.
Change the response-header policy while leaving the body unchanged and check that the validation response includes the cache fields that need updating. Compare HEAD and GET metadata too: they must not independently generate random tags. Leave unsupported protocol edge cases to the platform or a mature server instead of patching middleware with a seemingly sufficient string comparison. Correct conditional requests depend on the complete representation and the specified evaluation order. Omitting the body is only their visible result.
Verifying Performance Gains and Ensuring Correctness
curl -i https://example.dev/writing/a/
curl -i -H 'If-None-Match: "article-v7"' https://example.dev/writing/a/
First, read the current actual ETag, and then use it to send a conditional request. Do not blindly use example values. After modifying the body, the old tag should receive the new body and new tag. When the content remains unchanged, it should be validated according to the rules. Test both gzip and uncompressed representations, and observe whether the CDN modifies or removes the tag.
Furthermore, it’s important to ensure that the identity page prevents public sharing, to avoid the incorrect reuse of tags generated by different users. The “disk cache” in the browser panel and the 304 status code are different paths, and their interaction needs to be determined based on the request headers. The ultimate optimization goal is to reduce unnecessary data transfer while ensuring that updates are visible. A higher number of 304 responses doesn’t necessarily mean a faster overall experience.
The comparison rules themselves must also become independent counter-examples. Currently, using a strong tag "v7", a read request with If-None-Match: W/"v7", and a write request with If-Match: W/"v7" cannot use the same equality function. The former uses weak comparison, while the latter uses strong comparison. Removing W/ and then comparing would incorrectly allow write operations that depend on byte identity. This refers to standard comparison, not a suggestion to convert all business versions to weak tags.
Another sample uses If-None-Match: *. It checks if the target resource already exists and has the current representation, rather than simply matching the content to a version with a wildcard character. Reading and creating resources with conditions can lead to different pre-processing results. The test records the request method and the current resource state, and then verifies the response from the server being used. It is not sufficient to simply provide a string to the parser and claim that wildcard conditions are supported, ignoring the method semantics.
Finally, verify that the proxy does not modify these conditions. Older clients may carry headers containing multiple values; if the proxy truncates to the first value, the server receives a different set of preconditions. Use a synthetic list with two different labels to store the security test headers visible to both the entry and backend, and compare the protocol results. The security test headers for real users do not need to be fully logged; this acceptance focuses on the integrity of the middle layer, not on collecting browsing history.
References: HTTP Conditional Requests, HTTP Semantics RFC 9110.