A 304 Not Modified response can save bandwidth, but it is not automatically a performance win. This guide explains how HTTP revalidation works, how to compare ETag and Last-Modified strategies, where conditional requests help, and where they quietly add latency and complexity. If you have ever looked at DevTools and wondered whether those 304s are good news or a sign that your caching policy is too timid, this article gives you a practical framework for deciding.
Overview
At a high level, 304 Not Modified is part of the HTTP revalidation flow. A client already has a cached copy of a resource. Instead of downloading the full response again, it asks the server, in effect, “Has this changed?” If the answer is no, the server returns 304 Not Modified and sends no response body. The client reuses its local copy.
That sounds obviously beneficial, and sometimes it is. Revalidation can reduce bandwidth, lower origin egress, and help keep cached content fresh without forcing full downloads. It is especially useful when a resource may change, but not often enough to justify downloading it on every request.
But 304s are not free. A revalidation request still takes a network round trip. It still hits a browser cache layer, CDN, reverse proxy, or origin. It may still wake up application logic if caching headers are not configured well. For users on high-latency networks, a tiny 304 can feel slower than serving a resource directly from a fresh local cache. This is why the right question is not “Are 304s good?” but “For this resource, is revalidation better than freshness?”
To reason about this, it helps to separate three outcomes:
- Fresh cache hit: the browser uses the local copy without contacting the server.
- Revalidated hit: the browser contacts the server and gets
304 Not Modified. - Miss or changed resource: the browser downloads a new
200 OKresponse body.
In many production systems, the best experience comes from maximizing fresh cache hits for versioned static assets and using revalidation selectively for HTML, API responses, feeds, and other resources where freshness matters more than absolute speed.
Conditional requests usually rely on one or both of these validators:
- ETag, usually revalidated via
If-None-Match - Last-Modified, usually revalidated via
If-Modified-Since
Understanding how those validators behave is the key to understanding when HTTP revalidation helps or hurts.
How to compare options
If you are deciding between aggressive caching, ETags, Last-Modified, or a mix of all three, compare them through four practical lenses: latency, correctness, infrastructure cost, and operational complexity.
1. Latency: does the extra round trip matter?
This is the first filter, because performance is what most teams care about when they inspect 304 behavior. A fresh browser cache hit is usually faster than a 304 because it avoids the network. Even a lightweight validation request adds delay.
For example:
- A hashed JavaScript bundle like
app.84a9e2.jsusually should not revalidate often. If the filename changes on deploy, the old asset can be cached for a long time with strong freshness directives. - An HTML document for a dashboard or marketing page may benefit from short freshness windows plus revalidation, because users need reasonably current content.
- A JSON API endpoint that changes unpredictably might use validators if the payload is expensive to transfer, but a poor setup can simply move cost from bandwidth to latency and backend checks.
If you care about page speed, ask: would a short or long fresh cache lifetime be better than validating every time?
2. Correctness: how precisely can you detect change?
Conditional requests are only useful if your validator reflects the real state of the resource.
ETag is generally more precise because it can represent the exact version of the response. If the bytes change, the ETag changes. In practice, however, ETag generation varies. Some servers generate strong validators tied to exact content. Others generate weak or environment-specific values that can create surprises across nodes or compression variants.
Last-Modified is simpler, but less exact. It depends on timestamps, which can be coarse. If content changes multiple times inside the timestamp resolution window, or if clocks are inconsistent, the validator may not reflect the true update pattern.
If your application can cheaply and reliably compute content versions, ETag is often the better fit. If you need a low-friction validator and rough timestamp accuracy is acceptable, Last-Modified may be enough.
3. Infrastructure cost: where does the work happen?
A 304 with no body still consumes resources. The request may traverse a CDN, a load balancer, and your application stack. If your validation logic requires origin work, large volumes of revalidation can become expensive even when bandwidth usage stays low.
This matters in traffic spikes. A static asset served fresh from the browser avoids both transfer and backend work. The same asset revalidated by millions of clients may produce a storm of small but still real requests.
When reviewing a policy, consider:
- Can the CDN answer the validation request?
- Does the origin need to run app code to decide whether the resource changed?
- Are you reducing bytes while increasing request volume?
Bandwidth savings and request cost should be evaluated together.
4. Operational complexity: can your team debug it?
The more layers you have, the easier it is to misread cache behavior. Browsers, service workers, CDNs, reverse proxies, and application frameworks can all influence revalidation. A setup that looks tidy on paper may be hard to troubleshoot in production.
If your team regularly debugs stale content, validator mismatches, or cache inconsistencies across environments, a simpler strategy may be worth more than an elegant but fragile one. For a production-minded baseline, it helps to pair this article with a deployment review such as HTTP Caching Checklist for Production Sites.
Feature-by-feature breakdown
This section compares the main choices directly so you can decide which mechanism fits each resource type.
ETag vs Last-Modified
ETag answers the question “Is this exact representation the same?” Last-Modified answers the question “Has this resource changed since this time?” That difference sounds small, but it shapes real behavior.
ETag strengths:
- Can be highly accurate when tied to content versions
- Works well for byte-level identity checks
- Useful for resources where timestamp precision is not enough
ETag tradeoffs:
- Can be inconsistent across multiple servers if not generated deterministically
- May create unnecessary variation if tied to environment-specific metadata
- Sometimes harder to reason about than timestamps during debugging
Last-Modified strengths:
- Simple to implement and inspect
- Easy to understand in logs and browser tools
- Good fit when update times are stable and meaningful
Last-Modified tradeoffs:
- Less precise than content-based validators
- Dependent on timestamp quality and clock consistency
- Can miss edge cases involving fast successive updates
In practice, many systems support both. That gives clients more than one way to validate and can improve compatibility. Still, support for both should be intentional, not accidental. If the headers disagree semantically, debugging gets harder.
Revalidation vs long-lived freshness
This is often the more important comparison.
Long-lived freshness means the client can use the cached resource without contacting the server until freshness expires. This is usually the best option for versioned static assets such as:
- bundled JavaScript
- compiled CSS
- fonts
- images with content-hash filenames
For these resources, a 304 often indicates missed opportunity. If the URL changes whenever the file changes, you do not need frequent revalidation. Let the client reuse the file confidently.
Revalidation is more appropriate when:
- the URL stays stable while content may change
- users need current content soon after updates
- the response body is substantial enough that avoiding transfer matters
- the server or CDN can validate cheaply
Common examples include HTML documents, sitemap files, feeds, and some API responses.
Strong vs weak validation thinking
You do not always need perfect identity checks. Sometimes “changed recently enough” is sufficient. A useful way to think about validators is this:
- Use stronger validation when exact representation matters.
- Use simpler validation when approximate freshness is enough and the operational path is cleaner.
This mindset keeps teams from overengineering cache semantics for low-value resources while underprotecting high-value ones.
Browser, CDN, and origin behavior
A 304 can be generated at different layers. That matters because the performance impact depends on where validation occurs.
- Browser revalidating with CDN: often acceptable if the CDN responds quickly and shields origin traffic.
- Browser revalidating with origin: potentially costly at scale, especially for dynamic pages.
- Service worker involvement: can override or reshape browser behavior, making DevTools traces harder to interpret.
When debugging, inspect response headers carefully and note whether the validator check is happening at the edge or deeper in the stack. Teams dealing with layered systems often find that cache design becomes more important as architectures get more distributed. While focused on a different domain, the architectural discipline described in Blueprint for a unified caching layer across EHRs, CRMs and predictive services is a useful reminder that cache coherence problems grow with every additional layer.
Common failure modes
Several patterns make 304 behavior look healthy while hiding a deeper issue:
- Every request revalidates static assets. This reduces bytes but increases latency for resources that could have been served fresh.
- ETags differ across nodes. Clients download identical content repeatedly because validators are unstable.
- Last-Modified is present but meaningless. If the timestamp reflects deployment time rather than content updates, clients may validate too often or not often enough.
- Dynamic pages always return 304 after expensive app logic. You saved transfer, but not origin work.
- Validator headers conflict with cache-control intent. Teams think a resource is “cached,” but in reality it is validated on nearly every navigation.
If you see many 304s in logs, that is not automatically a sign of success. It may simply mean your freshness windows are too short for the resource type.
Best fit by scenario
Here is a practical way to choose the right strategy based on common web development workflows.
Scenario 1: Versioned static assets in a front-end build pipeline
Best fit: long-lived freshness, minimal revalidation.
If your build process outputs hashed filenames, treat that as your invalidation mechanism. Set a long cache lifetime and avoid forcing browsers to ask permission to reuse unchanged files. A 304 for these files often means you are giving up easy speed.
Good default mindset: if the URL changes on content change, let the cache be aggressive.
Scenario 2: Server-rendered HTML that may update between visits
Best fit: short freshness plus revalidation.
HTML is often the right place for conditional requests because the URL remains stable while content changes over time. Revalidation can prevent stale documents without forcing a full transfer for every visit. The key is to keep validation cheap and to avoid invoking unnecessary application work when the content has not changed.
Good default mindset: prioritize freshness, but monitor latency impact.
Scenario 3: Public API responses with moderately large payloads
Best fit: selective revalidation.
If responses are expensive to transfer and not updated constantly, validators can reduce bandwidth. But API consumers may prefer explicit cache behavior, and some endpoints are so dynamic that revalidation just adds overhead. Consider whether the payload, change frequency, and consumer expectations justify validator support.
Good default mindset: only add conditional requests where they remove meaningful work.
Scenario 4: CDN-backed media or files downloaded repeatedly
Best fit: long freshness if files are immutable, otherwise validator support at the edge.
For user-facing media, the ideal outcome is usually CDN or browser freshness, not constant validation. If files are mutable under stable URLs, ensure that the edge can answer validation efficiently. If not, the origin may absorb unnecessary traffic.
Good default mindset: keep validation away from the origin when possible.
Scenario 5: Frequently changing dashboards or admin surfaces
Best fit: resource-by-resource policy.
These interfaces often mix stable assets, semi-stable HTML, and highly dynamic API calls. Do not apply a single rule everywhere. Cache JS and CSS aggressively when versioned, revalidate HTML thoughtfully, and avoid pretend caching for endpoints that must be fresh every time.
Good default mindset: separate immutable assets from live data.
A simple decision rule
If you need a compact rule of thumb, use this:
- If the resource is versioned and effectively immutable, prefer long fresh caching over revalidation.
- If the URL is stable and updates matter, use validators.
- If validation requires expensive origin work, improve edge behavior or reconsider whether revalidation is worth it.
- If users experience sluggish repeat visits despite many 304s, your policy may be saving bytes while losing time.
When to revisit
Caching decisions age quickly because deployment models, traffic patterns, and application behavior change. Revisit your 304 and revalidation strategy whenever one of these shifts happens.
Revisit after build or deployment changes
If your asset pipeline starts producing hashed filenames, you can usually move many static files from revalidation-heavy policies to long-lived freshness. If you migrate from monolith to CDN-backed edge delivery, the validation path may also change enough to justify a review.
Revisit after infrastructure changes
New CDNs, reverse proxies, service workers, or framework-level cache features can alter how validators behave. A policy that was safe when all traffic hit one origin may become noisy or inconsistent in a distributed setup.
Revisit when debugging user-perceived slowness
If repeat visits still feel slow, open DevTools and look beyond transfer size. Check whether resources are truly served from fresh cache or whether they are being revalidated on each navigation. High counts of 304 responses can be a clue that your policy is conservative rather than efficient.
Revisit when backend costs rise
If you reduce bandwidth but request counts or origin checks remain high, your validators may be solving the wrong problem. Review whether revalidation can be terminated at the CDN or replaced with stronger freshness for stable resources.
A practical audit checklist
To make this article actionable, use this short audit on your next performance pass:
- List your main resource types: HTML, JS, CSS, images, fonts, API responses, downloads.
- For each one, note whether the URL changes when content changes.
- Check whether the browser is getting fresh hits, 304s, or full 200 responses on repeat visits.
- Identify which 304s are useful and which represent missed freshness opportunities.
- Verify that ETags are stable across environments if you use them.
- Verify that Last-Modified reflects meaningful content updates, not arbitrary deployment timestamps.
- Confirm whether validation is answered by the CDN or passed to the origin.
- Adjust one resource class at a time and test repeat-visit behavior again.
The goal is not to eliminate 304 responses. The goal is to use them where they create a good tradeoff between freshness, bandwidth, and latency. A healthy caching strategy usually includes all three states: fresh hits, selective revalidation, and occasional full fetches when content actually changes.
That is why 304 Not Modified is best understood as a tool, not a target metric. If a resource should be fresh, let it be fresh. If it needs validation, make the validator accurate and cheap. And if your stack grows more layered over time, document your assumptions so the next debugging session does not start from guesswork.