HTTP caching saves time and data by reloading resources only when they have actually changed. Via ETag and Last-Modified I use a conditional request to check whether the server responds with a 304 Not Modified status, which significantly reduces data transfer and server load.
Key points
The following key points highlight what I focus on when using conditional caching with ETag and Last-Modified eighth.
- Less traffic: Unmodified files return a 304 status code instead of the entire body—this significantly reduces data usage and latency.
- Better performance: Shorter wait times improve UX and Core Web Vitals, which SEO helps.
- Two mechanisms: Last-Modified/If-Modified-Since and ETag/If-None-Match reliably validate the cache.
- Cache control: Directives control freshness, revalidation, and behavior in intermediate caches.
- Combination: Together, these two methods offer high accuracy and simple fallbacks.
First, I check which resources change frequently and which ones rarely. For files that are rarely modified, I set a Last-Modified-time and add an ETag. For dynamic responses, I prefer to use the ETag, because any changes to the content are immediately noticeable. This way, I reduce the load on the servers, minimize latency, and deliver pages very quickly to returning visitors. This strategy strengthens the Core Web Vitals and thus, indirectly, visibility.
HTTP Conditional Caching: How to Check Validity
When the request is made again, the client sends additional headers along with the GET request, which I evaluate on the server side. If the resource has the same ETag If the content matches the cached version (If-None-Match), I return a 304 Not Modified response without a body. If the timestamp hasn’t changed (If-Modified-Since), the server also responds with a 304. If the day or date is no longer correct, I send a 200 OK with new content plus an updated Last-Modified and ETag. This saves bandwidth, keeps the cache up to date, and ensures noticeably faster load times.
Last-Modified and If-Modified-Since in Everyday Use
The header Last-Modified I rely on the actual modification time of the file, such as the one from the file system. If a request later arrives with an "If-Modified-Since" header and the resource hasn't changed since then, I respond with a 304. This approach is straightforward, easy to understand, and ideal for static assets like CSS, JS, or images. Limitations arise from the second-based grid of HTTP timestamps and situations where content changes logically without a clear file modification timestamp. Where Last-Modified reaches its limits, a ETag control.
ETag and If-None-Match in Dynamic Systems
A ETag I generate it as a hash, a version ID, or from a database column that tracks state changes. Upon subsequent access, the browser sends an If-None-Match header; I compare the tag with my current value and respond accordingly with a 304 or 200 status code. This comparison detects any meaningful content changes without relying on file timestamps. This delivers very accurate results, especially with APIs, composite pages, or personalized fragments. It remains important that I keep ETags consistent in cluster environments so that no server accidentally uses a different Day generated.
Combining Cache-Control Properly
With Cache control I define how long content is considered fresh without a request and when the browser revalidates it. I set appropriate max-age values based on the frequency of changes and use must-revalidate if outdated data would be critical. A long validity period is suitable for versioned files, while frequently changing responses have a shorter lifespan and can then be reliably verified using ETag or date. This is how I combine fast response times with accurate timeliness. If you want to delve deeper, you’ll find many examples at Cache-Control Strategies, that I use in my practice.
Step-by-Step Guide to a Conditional GET Request
On the first request, the server returns a 200 OK response with Cache-Control, Last-Modified and ETag; the browser stores everything. On the next visit, the age of the cached data determines whether revalidation is necessary. If revalidation is required, the browser sends a request using If-None-Match and/or If-Modified-Since. If the values match the current state, I send a 304 Not Modified response, and the client continues to use its cache. If they no longer match, a 200 OK response follows with a new body and updated Validation data.
Comparison: ETag vs. Last-Modified
Both methods give me control, but they differ in terms of effort, accuracy, and suitability. Last-Modified It stands out for its ease of implementation and clear semantics, as long as I have clean timestamps. The ETag accurately reflects the content, but requires some logic to generate. In many setups, I combine both and thus benefit from simplicity plus precise identification. The following table summarizes typical characteristics and helps with the decision.
| Aspect | Last-Modified | ETag | Note |
|---|---|---|---|
| Identity | Timestamp of the last change | Content hash or version ID | Time vs. content-based identifier |
| Change detection | Second resolution, indirect | Directly focused on the content | ETag detects the smallest Diffs |
| implementation | Very lightweight; the file system is sufficient | Requires generation and consistency | Clusters need the same ETags |
| Use | Static assets | Dynamic Responses | This combination covers many Cases from |
| Answers | 304 with the timestamp unchanged | 304 with the same date | 200 for changes with a new Value |
Best Practices: Efficiently Delivering Static Assets
Static files such as CSS, JavaScript, and images rarely change and are suitable for long-term use max-age-times. For versioned files, I set high values of up to one year and mark them as immutable so that the browser loads them without checking. For non-versioned assets, I choose shorter time limits and rely on revalidation via ETag and Last-Modified. This way, I avoid outdated content and keep traffic low. If I make sure not to Sabotage cache header By doing this, I achieve a high hit rate in the cache.
Practical Application: APIs and Dynamic Pages
When it comes to APIs, I usually rely on ETags, which I generate from the serialized result or a version column. If the data record changes, I generate a new tag, and clients recognize this immediately. For content with an unreliable timestamp, I often omit the Last-Modified header to avoid giving a false impression of recency. Additionally, I control the lifespan via Cache-Control and force revalidation upon expiration. This way, I reliably keep data fresh without making responses unnecessarily large.
Testing and Monitoring for Cache Hit Rate
I check headers such as ETag, Last-Modified, If-None-Match, and If-Modified-Since in the Developer Tools. I pay close attention to the response codes—especially 304 versus 200—to assess the effectiveness of my revalidation. If 304 is rarely encountered, I adjust Cache-Control, expiration times, and ETag generation. Logs and metrics show me which paths are returning unnecessarily large responses. For bundled improvements, I like to use a Conditional Requests Package, that combines configuration and testing.
Hosting Architecture and ETag Pitfalls
In multi-server setups, a ETag be independent of the instance, otherwise recognition will fail. I ensure that all nodes use the same logic and the same key for generation. Reverse proxies or CDNs must not modify ETags and should pass along condition headers correctly. For deployments using asset fingerprints, I avoid server-side ETag recalculation if the file already has a versioned URL. Consistent rules prevent inconsistent responses and keep the cache hit rate high.
Freshness vs. Validation: Using Directives Precisely
I make a clear distinction between freshness (How long can a cache use a copy without checking?) and Validation (How do I check if it's still valid?). About Cache control I control both with fine granularity: max-age specifies the lifetime on the client, s-maxage for shared caches such as proxies. public allows caching in shared caches, private it limits it to the end-user browser. must revalidate forces a prompt after expiration, while immutable prevents unnecessary revalidations for versioned assets. no-cache does not prohibit caching, but always requires revalidation; no-store on the other hand, completely prohibits saving. Older Expires- I only use headers as a fallback; I consistently rely on Cache-Control for the logic. And if I want to mitigate outages, stale-while-revalidate and stale-if-error, ...so I can share content that has recently expired while I update things in the background or work around errors.
Strong and weak ETags, compression, and variants
I deliberately distinguish between strong and weak validators. Strong ETags identify the exact same representation, byte for byte—ideal if I also Range Requests wants to operate efficiently. Weak ETags (Prefix W/) are sufficient when semantic equivalence is enough, such as in the case of minor, irrelevant formatting changes. What matters is how to handle Compression: If I serve both gzip- and Brotli-compressed content, a single ETag cannot apply to all variants. Either I generate the ETag based on the uncompressed version and also set an appropriate Vary: Accept-Encoding, or I generate consistent but unique ETags for each variant. This prevents false positives and 200 responses that should actually be 304s. In If-Range I combine range queries with a validator: If the ETag or date matches, I respond with a 206 Partial Content status code; otherwise, I return a 200 status code with the full body so that the client has a consistent baseline.
Mastering Vary headers and content negotiation
Whenever the server returns different representations depending on the request, I set Vary Correct. Typical candidates are Accept-Encoding (compression), Accept-Language (localization) or specific feature flags. I avoid using volatile headers such as User agent or even Cookie to vary, because that really messes up the cache hit rate. Where personalization is needed, I mark answers as private or no-store and clearly separate them from publicly cacheable resources. Important: Variations also affect ETags—each variant needs its own, consistent validator. This ensures that browsers, proxies, and CDNs apply the same logic and that no variant is accidentally mixed up with another.
Conditional requests beyond GET
Conditional requests don't just work when reading. For write methods, I use If-Match or If-Unmodified-Sincein order to missing updates to prevent this. If the client sends the last seen ETag in a PUT or DELETE request via If-Match If the server status is still the same, I'll make the change; otherwise, I'll respond with 412 Precondition Failed. To enforce discipline among clients, the server can also 428 Precondition Required set up. For quick tests without a body, I use HEAD, which returns the same headers as a GET request; ideal when I want to test metadata. And with 304-In my responses, I include all headers relevant to caching (Cache-Control, ETag, Expires, Last-Modified) so that the client can update its metadata without having to transfer the body.
Security, data protection and compliance
I do not store personal or sensitive content in the public cache. Here, I Cache control: private or no-store, so that the browser—or no instance at all—persists the content. Be careful with user accounts and dashboards: Responses with Set cookie or Authorization must not accidentally be publicly cacheable. ETags themselves can be misused as a tracking vector if they remain stable over a long period of time. I address this by actively using validators only where caching is intended, and by disabling them for user-specific routes or keeping their lifespan short. This way, I balance performance with data protection requirements.
Implementation details and performance costs
Generating an ETag should not cost more than the benefit it provides. For large files or expensive renders, I store the tag along with metadata (file checksum, build hash, database—row version) and don't re-render it with every request. For composite pages, a Versioning Strategy: I construct the ETag from stable sub-ETags (e.g., template, data fragment, configuration) so that small changes result in a specific but reproducible new value. In clusters, I synchronize the generation logic in a shared library and test it in CI to ensure no instance deviates. For extremely large blobs, I rely on fast checksums (CRC64) or store build hashes instead of hashing the body on the fly. Where absolute byte-for-byte equality is not necessary, weak ETags as a pragmatic compromise.
Common mistakes and how to avoid them
- Random ETags: If tags are regenerated with every request, revalidation is pointless. I ensure that values are deterministic and only change when there is an actual change.
- Incorrect combination of directives: no-store Using ETag doesn't help—the browser doesn't cache the data anyway. I choose consistent combinations to achieve the desired behavior.
- Excessive Vary: Variations in the Cookie or User-Agent headers break the cache. I restrict Vary to genuine changes in representation.
- Compression traps: A shared ETag for gzip and br results in false positives. I properly associate ETags with the specific variant and set the Vary header correctly.
- Time drift: Inaccurate server clocks can skew the "Last-Modified" header. I keep time sources synchronized so that "If-Modified-Since" works correctly.
- Confusion regarding "no-cache": Many people read this as „do not cache.“ What is meant is „always revalidate.“ For a true prohibition, I use no-store.
Troubleshooting, Metrics, and Workflows
To troubleshoot, I start in the Network tab: Is this correct? Cache control? Occurs during rehabilitation 304 Instead of 200? That works. ETag and Last-Modified between the request and the response? I'll check Vary, to see if variants are detected correctly. In the logs, I check Hit or Miss-Output hit rates, 304 rates, and average response sizes per path. If the 304 rate increases, data volume and TTFB typically decrease noticeably. In load tests, I simulate repeat requests to measure revalidation costs rather than transfer costs. If anomalies arise, I gradually eliminate interfering factors: Set-Cookie, overly strict Vary rules, conflicting headers like Pragma. This allows me to quickly identify the bottleneck that is dragging down the hit rate.
Service Workers as a supplementary caching layer
When I use a service worker, I use it as an additional layer, not as a conflicting one. I let it handle the same Cache control-Respect signals and combine strategies such as stale-while-revalidate deliberately uses HTTP validation via ETag and Last-Modified. For offline scenarios, the worker can serve temporarily outdated resources and revalidate them in the background. It remains important that it correctly passes on the condition headers; otherwise, I lose the benefits of 304 over the network. This way, PWA scenarios also benefit from proper HTTP caching instead of circumventing its mechanisms.
SEO Impact and Core Web Vitals
Improve quick replies UX and user signals, which improves rankings. Returning visitors benefit in particular because their browsers retrieve many files directly from the cache or via a 304 response. This reduced latency has a positive effect on FCP, LCP, and TTFB, which I lower through targeted revalidation. In addition, the server saves processing time, which I can use for peak loads or complex requests. This allows me to maintain performance while ensuring content is delivered correctly and promptly.
Summary: My Action Plan
I rely on a clear Combination based on Cache-Control, Last-Modified, and ETag. For static assets, I set long expiration times and use revalidation as a safeguard when files aren’t versioned. For dynamic responses, I generate robust ETags and maintain cluster consistency. I then use tools, metrics, and logs to check whether 304 responses occur frequently enough and adjust settings accordingly. This ensures fast delivery, lower load, and a better user experience through effective HTTP Caching.


