HTTP 304 Not Modified, Simulated Request by Request

A 304 Not Modified reply tells the browser its cached copy is still current, so the server skips sending the body again. It answers a conditional GET or HEAD that carried If-None-Match or If-Modified-Since. A 304 is good news, not an error: it saves bandwidth while the cached file keeps being used.

The validator pairs behind a 304
Server sendsBrowser sends backServer answers 304 when
ETag: "v2"If-None-Match: "v2"One listed tag matches the current ETag (weak comparison)
Last-Modified: <date>If-Modified-Since: <date>The resource has not changed since that date
BothBothIf-None-Match decides; If-Modified-Since is ignored

Updated

Fill in what the server currently holds and what the browser sends, and the simulator applies the precedence rules from the HTTP specification. It shows the status the server should return and which header decided it. Blank a field to leave that header out.

Conditional request simulator

What the server holds now

ETag

Last-Modified

What the browser sends

If-None-Match

If-Modified-Since

Server answer

304 Not Modified

If-None-Match lists a tag that matches the current ETag under weak comparison, so the cached copy is current.

The exchange on the wire

First visit: 200 with validators

The server hands out an ETag and a Last-Modified date along with the content.

GET /styles.css HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: no-cache
ETag: "v2"
Last-Modified: Tue, 13 Oct 2026 09:00:00 GMT

Next visit: 304 with no body

The browser sends the saved tag back and gets headers only.

GET /styles.css HTTP/1.1
Host: example.com
If-None-Match: "v2"

HTTP/1.1 304 Not Modified
Cache-Control: no-cache
ETag: "v2"

Answering conditional requests yourself

Node http server with a content hash ETag

Hashes the body, compares it with If-None-Match and sends 304 with no body on a match.

import { createServer } from "node:http";
import { createHash } from "node:crypto";

const body = JSON.stringify({ items: [1, 2, 3] });
const etag = '"' + createHash("sha1").update(body).digest("hex") + '"';

createServer((req, res) => {
  res.setHeader("ETag", etag);
  res.setHeader("Cache-Control", "no-cache");
  const tags = (req.headers["if-none-match"] || "")
    .split(",")
    .map((t) => t.trim().replace(/^W\//, ""));
  if (tags.includes(etag)) {
    res.statusCode = 304;
    return res.end();
  }
  res.setHeader("Content-Type", "application/json");
  res.end(body);
}).listen(3000);

How revalidation works

When a cached response goes stale, the browser does not throw it away. It asks the server whether its copy is still good by sending the validators it saved: the ETag as If-None-Match, the Last-Modified date as If-Modified-Since.

If nothing changed, the server replies 304 with fresh headers and no body, and the browser keeps using its copy. If something changed, the server sends a normal 200 with the new content.

Strong and weak tags

A strong tag such as "v2" promises the bytes are identical. A weak tag such as W/"v2" promises only that the content is equivalent. If-None-Match compares weakly, so W/"v2" and "v2" count as a match there; If-Match compares strongly and would not.

How it works

  1. Set the ETag and Last-Modified value the server has for the file right now.
  2. Set the If-None-Match and If-Modified-Since headers the browser sends from its cache.
  3. Read the status and the reason: which header was evaluated and why it matched or not.
  4. Change the server ETag to see how a deploy turns the next request into a 200.

Tips

Frequently asked questions

Is a 304 response an error?

No. It is a redirection-class status meaning the cached copy may be used. The browser shows the page from its cache, and only headers crossed the network.

Can a 304 response have a body?

No. The specification says a 304 response ends at the end of the header section and cannot contain content.

Which wins, If-None-Match or If-Modified-Since?

If-None-Match. When a request carries both, a server that supports entity tags evaluates If-None-Match and ignores If-Modified-Since, because tags detect changes that a one-second date cannot.

Why do I get 200 instead of 304 after every deploy?

The deploy changed the file or its ETag, so the tag the browser holds no longer matches. That is correct behaviour. If unchanged files also lose their 304, check whether your servers generate different ETags for the same content.

Does POST ever return 304?

No. If-None-Match and If-Modified-Since only produce 304 for GET and HEAD. For other methods a failed condition gives 412 Precondition Failed.

Related: HTTP status codes reference, HTTP 429 Too Many Requests, CORS explained, JavaScript fetch with async and await.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap