What Is a 304 Status Code? | Stop Wasted Reloads

A 304 response means the page hasn’t changed since your last visit, so your browser can reuse its cached copy and skip downloading it again.

You’ve seen it in DevTools. A request fires, the response comes back as “304,” and the page still loads. No error. No visible change. Just that little code sitting in your network log.

That’s the point. A 304 is a speed move. It tells the browser, “You already have the bytes. Use them.” The server still gets a request, but it avoids sending the full body again. Less bandwidth. Less time. Less load.

This article breaks down what a 304 really means, which headers decide it, how it behaves with CDNs and proxies, and what to check when 304s show up in places you didn’t expect.

304 Status Code Meaning In Browser Caching

HTTP status 304 is “Not Modified.” It’s part of conditional requests, where the browser asks for a resource while hinting, “Only send the full thing if it changed.”

When the server agrees the resource is unchanged, it replies with 304 and no response body. The browser then uses its cached copy to render the page or asset.

That leads to a small mind shift: a 304 is not a “success response with content.” It’s a “success response with a decision.” The decision is: don’t transfer the body.

Why 304 Exists

Caching needs a safe way to reuse content without guessing. If the browser reused cached files forever, users would see stale pages. If it redownloaded everything every time, pages would feel slow and expensive to serve.

304 sits in the middle. The browser checks in. The server confirms the cached copy is still valid. Then the browser reuses it.

What A 304 Is Not

  • It’s not a redirect. The URL doesn’t change.
  • It’s not an error. Your request was understood.
  • It’s not “the server did nothing.” The server compared validators and made a call.
  • It’s not “cache always works.” A 304 can still be misconfigured or noisy.

What Is a 304 Status Code?

You’ll usually get a 304 when a client sends a conditional header like If-None-Match (ETag-based) or If-Modified-Since (timestamp-based). The server checks the current version of the resource against what the client claims to have.

If the match holds, 304 is sent and the response body is omitted. HTTP semantics for this flow are defined in the IETF’s HTTP specifications, including the rules around validators, conditional headers, and caching behavior. HTTP Semantics (RFC 9110) lays out the core meaning of 304 and the headers around it.

304 Versus 200 From Cache

There are two “cached” outcomes that can look similar in DevTools:

  • Served from cache without a network trip: the browser decides the cached copy is fresh and doesn’t revalidate. You may see “(from disk cache)” or “(from memory cache).”
  • Revalidated with 304: the browser checks with the server, gets 304, then uses its cached bytes.

Both can be fine. The difference is whether the browser felt it needed to re-check. That decision comes from cache headers like Cache-Control, plus the resource’s age and the browser’s own heuristics.

Headers That Decide Whether You Get 304

304 behavior is driven by two groups of headers:

  • Validators: signals that identify a version of a resource (ETag, Last-Modified).
  • Cache policy: rules that say when the browser must revalidate (Cache-Control, Expires, Vary).

If you want a clear, practical description of these headers in plain terms, MDN’s documentation is one of the best quick references for developers working with browser tooling. MDN: 304 Not Modified summarizes how browsers treat it and which request headers trigger it.

ETag And If-None-Match

An ETag is a token that represents a specific version of a resource. The browser stores it with the cached response.

On a later request, the browser may send If-None-Match: "etag-value". If the server’s current ETag matches, the server returns 304.

ETags are powerful, but they’re also easy to mess up. If your server generates unstable ETags (like values that change on every request), you lose the benefit and may trigger extra downloads.

Last-Modified And If-Modified-Since

Last-Modified is a timestamp. The browser stores it, then later sends If-Modified-Since when it wants to revalidate. If the resource hasn’t changed since that time, the server returns 304.

Timestamps are simpler than ETags, but they can be less precise. Some deploy flows update timestamps even when the bytes stay the same, which can force unnecessary 200 responses.

Cache-Control, Expires, And Vary

Cache-Control sets the rules. A long max-age can prevent revalidation for a while. no-cache is a common gotcha: it does not mean “don’t store.” It means “store, but revalidate before reuse.” That pattern often produces lots of 304s.

Vary tells caches which request headers create separate variants. If you vary on Accept-Encoding or Accept-Language, your cache can split into multiple versions, changing how often revalidation happens.

Google’s performance docs cover caching patterns that keep sites snappy while still staying correct. The guidance is especially useful when you’re setting cache lifetimes for static assets. web.dev: HTTP caching explains how browsers decide to reuse or revalidate responses.

Header Or Concept What It Does What You’ll See With 304
ETag Version token for a response. Browser sends If-None-Match; matching token leads to 304.
If-None-Match Client asks for body only if ETag changed. Present on the request that triggers 304 checks.
Last-Modified Timestamp of last known update. Browser may send If-Modified-Since; server can reply 304.
If-Modified-Since Client asks for body only if changed since a date. Common with static files on simpler stacks.
Cache-Control: max-age How long a cached copy is fresh. Long values can reduce 304 frequency by skipping revalidation.
Cache-Control: no-cache Cached copy may be stored, but must be revalidated. Often increases 304 volume, since revalidation happens often.
Vary Splits cache by request headers that affect the response. Different variants can revalidate at different times.
Age How long the response has been cached by an intermediary. Helps explain cache behavior when a CDN sits in front.
Date Timestamp of when the response was generated. Used with cache rules to estimate freshness.

When Seeing 304 Is A Good Sign

Most of the time, a 304 is exactly what you want. You’ll see it a lot on:

  • CSS, JS, images, fonts
  • API responses that are cacheable and stable
  • Pages that are cacheable but revalidated often

On a busy site, 304 responses can trim outbound bandwidth by a lot. They also cut time spent sending bytes over the network. The server still does some work, yet far less than building and sending full responses repeatedly.

Why Your Page Can Feel Fast With Lots Of 304s

If your cache is warm, the browser already has the bytes on disk. A 304 response is small, so the round-trip is quick. Rendering can start right away once the cached files are reused.

In practice, the main cost is the extra request. That’s why strong cache lifetimes for static assets still matter. If you can safely keep assets fresh for longer, you reduce network chatter, not just bandwidth.

When 304 Becomes A Problem

A 304 isn’t “bad,” but your setup can make it noisy or misleading. Here are the patterns that tend to cause pain.

ETags That Change When Nothing Changed

Some stacks generate ETags using inode values, timestamps, or load-balancer behavior. Two servers behind a balancer may compute different ETags for the same file. The browser then gets bounced into unnecessary 200 downloads.

Flip that around and you can get the opposite issue: the server claims “not modified” while the content did change, usually due to a broken validator. That leads to users seeing stale assets until cache is cleared.

Over-Revalidation From Cache-Control

If you apply Cache-Control: no-cache broadly, the browser will revalidate many resources on each visit. That means lots of 304s. It may still be acceptable, but it can raise request counts, add latency on high-latency links, and increase server hits.

CDN Or Proxy Behavior Hiding The Real Story

A CDN may answer on behalf of your origin, including returning 304. That can be fine, but you need to know who is responding. Look at response headers like Via, Server, and vendor-specific cache headers to confirm where the decision is made.

Cloudflare’s docs are a solid reference when you’re checking how edge caching and revalidation work on a popular CDN. Cloudflare: Default cache behavior explains what it caches by default and how it handles freshness.

How To Confirm A True 304 In Your Tools

It’s easy to misread 304 when you’re moving quickly. Do these checks to confirm what’s happening.

Check Request Headers First

In your browser’s network panel, click the request and find the request headers. Look for one of these:

  • If-None-Match
  • If-Modified-Since

If neither is present, you’re likely not in a true conditional request flow. Some tools label cached behavior in ways that can confuse, so reading headers is the cleanest path.

Check Response Headers Next

For a real 304 response, you should see:

  • Status 304
  • No response body
  • Often an ETag or Last-Modified header still present
  • Cache-Control still matters for the next reuse decision

Reproduce With Curl

If you want a quick reproduction outside the browser, you can:

  1. Fetch headers once and note ETag or Last-Modified.
  2. Send a second request with If-None-Match or If-Modified-Since.

You’ll see a 304 if the validator matches. If you get 200, the server decided the resource changed, or it doesn’t support conditional requests for that route.

Common 304 Scenarios And What To Check

Here’s a practical map of where 304 shows up and what usually fixes the weird cases. Use it as a triage list when you see unexpected behavior in production logs.

Where You See 304 What It Often Means What To Check
Versioned assets like app.9c21.js Revalidation still happens due to cache policy. Set long max-age with immutable assets; confirm deploy adds new filenames.
HTML document requests Browser wants to avoid stale pages. Use short cache lifetimes for HTML; keep assets cached longer than pages.
API responses Client is reusing cached data. Confirm ETag logic matches the serialized payload, not request timing.
Images behind a CDN Edge is revalidating with origin. Review CDN cache rules; check origin Cache-Control and validator stability.
Lots of 304 on every page load Revalidation is required often. Audit no-cache usage; verify max-age settings per file type.
Users report stale CSS or JS Cache is reusing an older file version. Use filename hashing; avoid reusing URLs for changed asset contents.

How To Set Caching So 304 Helps, Not Hurts

Cache policy isn’t one-size-fits-all. A sensible setup treats HTML differently from static assets.

Use Short Lifetimes For HTML

HTML changes often. It controls which CSS and JS versions the browser loads. If HTML is cached too long, users may not receive a new build reference.

A common pattern is a short max-age for HTML and revalidation, paired with longer caching for static assets. That keeps pages correct while keeping assets sticky.

Use Long Lifetimes For Hashed Assets

If your build pipeline includes content hashing in filenames, you can cache those assets for a long time. When the content changes, the filename changes too, so the old asset can remain cached without breaking anything.

In that setup, you should see fewer 304s for versioned assets. The browser just uses the cached copy until the URL changes.

Be Careful With Shared Caches And Personal Data

Responses that depend on logged-in state, cookies, or per-user content need careful cache headers. Shared caches can store responses that were meant for a single user if headers are wrong.

Use Cache-Control: private where appropriate, and only allow shared caching for truly public content. If you’re unsure, err on the side of not caching personalized HTML at shared layers.

A Practical Checklist For Debugging 304 In Production

When a 304 cluster appears in logs or metrics, use this checklist to get clarity fast.

  1. Confirm validators: check if responses include ETag or Last-Modified.
  2. Confirm conditional requests: look for If-None-Match or If-Modified-Since in request headers.
  3. Check cache policy: scan Cache-Control, Expires, and Vary.
  4. Check for multi-server drift: compare ETags across nodes behind a balancer.
  5. Confirm CDN role: look for edge cache headers and whether the edge or origin is returning 304.
  6. Test a hard reload: confirm whether the server can return a full 200 when forced to bypass cache.

What You Want To See Over Time

For static assets, you want fewer validations and more cache hits without network requests. For HTML, you want correct content with minimal overhead. A healthy mix usually looks like:

  • Long-lived cached assets that rarely revalidate
  • Occasional 304s for pages and unversioned assets
  • Very few full 200 downloads for files that didn’t change

When those three align, 304 becomes a quiet workhorse: it keeps pages feeling quick, keeps bandwidth low, and keeps your origin from doing extra work just to resend identical bytes.

References & Sources

  • RFC Editor (IETF).“RFC 9110: HTTP Semantics.”Defines HTTP status codes, validators, and the rules behind conditional requests that produce 304 responses.
  • MDN Web Docs.“304 Not Modified.”Explains what 304 means in browser requests and how caching and conditional headers trigger it.
  • web.dev (Google).“HTTP caching.”Breaks down cache headers and browser reuse vs revalidation behavior that affects 304 frequency.
  • Cloudflare Developers.“Default cache behavior.”Describes how a CDN edge handles caching and freshness decisions that can influence 304 responses.