Back to HTTP Headers

If-None-Match conditional request

Makes a request conditional on an ETag NOT matching — the standard mechanism behind HTTP caching revalidation (304 Not Modified).

What it does

If-None-Match makes a request conditional on the resource's current ETag not matching one or more ETags supplied by the client. It's the mechanism behind HTTP's most common caching pattern: a client that already has a cached copy of a resource sends its stored ETag back, and the server either confirms nothing has changed (304 Not Modified, no body sent) or returns the new version (200 OK with the full response) if it has.

This is genuinely one of the highest-impact headers for real-world performance — it's what lets a browser skip re-downloading a resource it already has, while still guaranteeing it never serves stale content, since the check happens on every request rather than relying purely on a time-based cache expiry.

It's also used, less commonly but importantly, for the opposite purpose on write requests: If-None-Match: * to ensure a PUT/POST only succeeds if the resource doesn't already exist — an atomic "create only if absent" check.

Syntax

If-None-Match: "<etag-value>"
If-None-Match: "<etag-value>", "<etag-value>"
If-None-Match: *

Can list one or more ETags (comma-separated) — the condition passes (request proceeds normally) only if none of them match the resource's current ETag. * is a special value meaning "any existing representation" — used specifically to check for non-existence.

Both strong and weak ETags are valid here, and weak comparison is used (a W/ weak ETag is treated as matching its strong counterpart if the underlying content is judged equivalent) — this is a deliberate difference from If-Match, since a conditional GET only needs to know "was there a meaningful content change," not byte-for-byte identity.

How it powers caching (304 Not Modified)

The standard revalidation flow:

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

HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d"
Cache-Control: max-age=3600
Content-Type: text/css

<full CSS content>

Later, once the cached copy needs revalidation:

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

HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d"

304 has no body — the client just extends the freshness of its existing cached copy. This saves the full response payload on every revalidation, which matters enormously at scale (a CSS/JS bundle that hasn't changed doesn't need to be re-transferred just because its cache expired).

Using it for "create only if absent"

PUT /users/alice HTTP/1.1
If-None-Match: *
Content-Type: application/json

{"name": "Alice"}

If a resource already exists at that URL, the server responds 412 Precondition Failed — the write is rejected rather than silently overwriting existing data. If nothing exists there yet, the write proceeds. This is a clean way to implement atomic "create, don't overwrite" semantics without a separate existence-check request (which would have a race condition between the check and the write anyway).

Common mistakes and gotchas

Confusing this with If-Match. If-Match proceeds when the ETag does match (used to protect updates against concurrent modification). If-None-Match proceeds when it doesn't match (used for cache revalidation, and inverted for existence checks). They're conceptual opposites serving different use cases — mixing them up silently breaks either your caching or your concurrency control.

Not sending the ETag exactly as received. The value must be sent back byte-for-byte, quotes included — If-None-Match: "abc123" not If-None-Match: abc123. A client that strips or reformats the quotes will never get a match, defeating caching entirely (every request falls through to a full 200 response).

Forgetting that weak comparison applies here. Unlike If-Match, If-None-Match uses weak comparison by default — a W/"abc" ETag from the server will match a client's cached W/"abc" (or vice versa in some implementations), meaning semantically-equivalent-but-not-byte-identical content correctly triggers 304. If your application generates ETags but doesn't understand this distinction, you might see more 304s than expected — which is usually fine, since that's the point.

Server not implementing conditional GET at all. Just because a server sends an ETag doesn't guarantee it correctly evaluates If-None-Match on subsequent requests — some naive implementations return 200 with a full body every time regardless of a matching ETag, silently wasting the entire benefit of conditional requests.

Real-world examples

Standard cache revalidation (unchanged resource):

GET /api/products/42 HTTP/1.1
If-None-Match: "v3-a1b2c3"

HTTP/1.1 304 Not Modified
ETag: "v3-a1b2c3"
Cache-Control: max-age=300

Revalidation where the resource changed:

GET /api/products/42 HTTP/1.1
If-None-Match: "v3-a1b2c3"

HTTP/1.1 200 OK
ETag: "v4-d4e5f6"
Content-Type: application/json

{"id": 42, "price": 19.99}

Atomic create, rejected because resource exists:

PUT /documents/report-2026 HTTP/1.1
If-None-Match: *

HTTP/1.1 412 Precondition Failed

FAQ

What's the difference between If-None-Match and If-Modified-Since?

Both drive 304 Not Modified caching, but If-None-Match compares ETags (content-based) while If-Modified-Since compares timestamps (time-based). ETags are more precise — a resource can be regenerated with identical content and get the same ETag (correctly triggering 304), whereas If-Modified-Since would see a new timestamp and incorrectly trigger a full re-send. When both are present, If-None-Match takes precedence per spec.

Why would I use If-None-Match: * for a write instead of just checking if the resource exists first?

Checking existence with a separate GET, then writing, has a race condition — another request could create the resource in between your check and your write. If-None-Match: * performs the check atomically as part of the write itself, closing that race window entirely.

Does If-None-Match use strong or weak comparison?

Weak comparison — a W/ weak ETag can satisfy the match check against its strong equivalent. This is intentional: cache revalidation only cares whether the content is meaningfully the same, not byte-identical, unlike If-Match's stricter strong-comparison requirement for write safety.

Do I need to implement If-None-Match manually, or does my framework handle it?

Most modern web frameworks provide built-in conditional-GET support (generate an ETag, check If-None-Match, return 304 automatically) if you opt in — but plenty of custom API endpoints or hand-rolled responses skip this entirely, leaving real caching performance on the table. Worth checking whether your framework's conditional-request middleware is actually enabled.

Fun fact

If-None-Match-driven 304 responses are one of the few places in HTTP where a request can succeed with genuinely zero response body — not even a small one — while still fully answering the client's question. On a high-traffic site, the aggregate bandwidth saved by turning what would be full 200 responses into empty 304s can be enormous; it's a big part of why ETags remain relevant even in an era of aggressive edge caching and CDNs, since revalidation still needs to happen somewhere once a cached copy's max-age expires.