Back to HTTP Status Codes

304 Not Modified 3xx

The requested resource has not changed since the version specified by the client, who can use their cached copy.

What does 304 mean?

A 304 Not Modified response means the client's cached copy of a resource is still valid — the server is confirming "nothing has changed since you last fetched this, go ahead and use what you already have." It's the response to a conditional request, where the client tells the server "here's what I have, and when/how I got it — has it changed?"

304 is unusual among status codes in that it's fundamentally about efficiency rather than success or failure in the usual sense — it's a deliberate mechanism to avoid re-sending data the client already has, saving bandwidth and improving load times.

How conditional requests and 304 work together

This is the key mechanism behind 304:

  1. The client first requests a resource normally and receives a 200 response, along with caching-related headers like ETag (a fingerprint/version identifier for the resource) and/or Last-Modified (a timestamp)
  2. The client caches the resource along with these headers
  3. On a subsequent request for the same resource, the client sends If-None-Match: <etag> (or If-Modified-Since: <timestamp>) — essentially asking "has this changed since the version I have?"
  4. If the resource hasn't changed, the server responds with 304 Not Modified and no body — the client uses its cached copy
  5. If the resource has changed, the server responds normally with 200 and the new content (plus updated ETag/Last-Modified headers)

How a 304 behaves

  • It has no response body — there's nothing to send, since the point is "you already have this"
  • It can still include headers — particularly an updated ETag if relevant, and cache-control directives
  • It requires the client to have sent a conditional requestIf-None-Match or If-Modified-Since — a normal, non-conditional request will never receive a 304
  • It's a performance optimization, not an error — from the user's perspective, the page/resource simply loads (from cache), often faster than a full 200 response would

Common scenarios

If you're building the API or website:

  • Set ETag and/or Last-Modified headers on responses for resources that don't change on every request (static assets, API responses for relatively stable data)
  • Check If-None-Match/If-Modified-Since headers on incoming requests, and return 304 (with no body) when the resource matches what the client already has — this is often handled automatically by web servers/CDNs for static files, but may need explicit implementation for dynamic API responses
  • CDNs and reverse proxies often handle conditional requests for static assets automatically; for dynamic content, the application itself may need to generate ETags and check conditional headers

If you're calling an API:

  • If you're caching API responses, store the ETag/Last-Modified headers alongside the cached data, and send them back as If-None-Match/If-Modified-Since on subsequent requests for the same resource — a 304 response means you can keep using your cached data without re-parsing a new payload
  • Many HTTP client libraries and browser fetch/XHR implementations handle this automatically when a cache is configured, without requiring manual header management

If you're a website visitor:

  • 304s happen constantly and invisibly — every time a page loads faster because "it was already cached," conditional requests and 304 responses are often part of why
  • You won't see a 304 directly, but browser developer tools' Network tab will show 304 responses for resources served from cache via conditional requests (as opposed to resources served entirely from local cache without even contacting the server)

304 vs 200 (from cache)

304 Not Modified 200 served from local cache
Server contacted? Yes — a conditional request was sent and the server responded No — the browser used its cache without contacting the server at all
Response body None Full body (from local cache, not re-downloaded)
When it happens Cache entry's freshness has expired, but content is unchanged Cache entry is still considered "fresh" per Cache-Control/max-age — no request needed at all

Both result in the client using its existing cached content — the difference is whether a request was made to verify it's still valid.

Real-world examples

CDNs and static asset hosting (CSS, JS, image files with content-hash-based filenames) commonly rely on ETag/Last-Modified plus 304 responses as part of their caching strategy — a browser revisiting a site can quickly confirm via a small conditional request that its cached assets are still current, without re-downloading potentially large files. Some APIs that serve frequently-polled but infrequently-changing data (configuration endpoints, feature flags) support conditional requests so polling clients can check for updates efficiently without repeatedly downloading unchanged payloads.

SEO implications

304 responses are generally good for SEO and site performance — they reduce the bandwidth and time needed for search engine crawlers (and users) to confirm that previously-fetched content is still current, which can contribute to faster perceived load times and more efficient use of a site's crawl budget (crawlers can verify more pages are unchanged without fully re-downloading each one). Sites that properly implement ETag/Last-Modified headers and respond to conditional requests with 304 where appropriate are generally considered to be following good caching practices.

FAQ

Do I need to do anything special to get 304 responses?

For static assets, many web servers and CDNs handle conditional requests and 304 responses automatically. For dynamic content (API responses, server-rendered pages with frequently-changing data), you may need to explicitly generate ETag/Last-Modified headers and check conditional request headers in your application code.

What's the difference between a 304 and content being served entirely from local browser cache with no server request at all?

A 304 means the browser did contact the server (with a conditional request) and the server confirmed nothing changed. Content served "from cache" with zero network activity means the browser's cache considered the content still fresh based on Cache-Control/max-age and didn't need to ask the server at all. Both avoid re-downloading the full content, but only one involves a round-trip to the server.

Can a 304 response include updated headers even though the body is unchanged?

Yes — while the body is always empty on a 304, headers like ETag (if it's been regenerated but represents equivalent content) or cache-control directives can still be included and updated.

Is ETag or Last-Modified better for conditional requests?

ETag is generally considered more precise — it's a fingerprint of the actual content, so it can detect any change, including ones that don't affect a timestamp. Last-Modified is simpler (just a timestamp) but has lower precision (typically second-level) and can occasionally produce false positives/negatives around exact timing. Many implementations use both together for robustness.

Why don't I see 304s for every page I revisit?

If a resource's Cache-Control/max-age says it's still "fresh," the browser won't even make a request — it'll use its local cache directly with zero server contact, skipping the conditional-request/304 mechanism entirely. 304s specifically happen when the cache has expired (so a request is made) but the content turns out to be unchanged.

Fun fact

304 is one of the few status codes that's considered a success despite returning no content and, in a sense, "doing less" than a normal response — it represents one of HTTP's earliest built-in optimizations for the exact problem that would otherwise plague the web at scale: needlessly re-downloading the same unchanged files over and over again.

Related Status Codes