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.
| Server sends | Browser sends back | Server 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 |
| Both | Both | If-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.
ETag
Last-Modified
If-None-Match
If-Modified-Since
Server answer
304 Not ModifiedIf-None-Match lists a tag that matches the current ETag under weak comparison, so the cached copy is current.
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 GMTThe 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"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);
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.
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.
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.
No. The specification says a 304 response ends at the end of the header section and cannot contain content.
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.
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.
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.
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.