An ETag (short for "entity tag") is a unique identifier that a web server attaches to a specific version of a file or resource, letting browsers ask "has this changed?" before downloading it again. When the answer is no, the server sends a tiny
304 Not Modified
response instead of the full file, saving bandwidth and speeding up page loads. That simple back-and-forth is the heart of how ETags improve HTTP caching.
Content Table
What are ETags
An ETag is a string of characters the server generates to represent one exact version of a resource. Think of it like a fingerprint: change a single byte in the file, and the ETag should change too. The value is opaque, meaning the browser never tries to interpret it. It only compares one ETag against another.
A typical response header looks like this:
HTTP/1.1 200 OK
Content-Type: text/css
ETag: "a3f5c9e8b1d2"
Cache-Control: max-age=3600
Servers usually build ETags from one of a few things:
- A hash of the file contents (for example an MD5 or SHA digest).
- The file's last-modified timestamp combined with its size and inode.
- A version number the application assigns to the resource.
The exact recipe is up to the server. What matters is consistency: the same content should always yield the same ETag, and different content should yield a different one.
How ETags work step by step
ETags shine during cache validation, the process where a browser checks whether its cached copy is still good. Here is the full round trip:
-
The browser requests a file for the first time. The server responds with
200 OK, the file body, and an ETag header like"a3f5c9e8b1d2". - The browser stores both the file and its ETag in the cache.
-
Later, when the cached copy expires, the browser sends the same request but adds an
If-None-Matchheader carrying the stored ETag. - The server compares the incoming ETag with the resource's current ETag.
-
If they match, the server replies
304 Not Modifiedwith no body. The browser reuses its cached file. -
If they differ, the server sends a fresh
200 OKwith the new content and a new ETag.
That
304
response is often just a few hundred bytes, compared to a full file that might be tens or hundreds of kilobytes. Multiply that across every CSS file, image, and script on a page, and the savings add up fast. If you want a refresher on what these codes mean, our guide to
HTTP status codes and what each number means
breaks down the 2xx and 3xx families in detail.
Cache-Control: max-age
instead, paired with
cache busting
so a changed file gets a new URL.
Strong vs weak ETags
The HTTP spec defines two flavors of entity tags, and the difference affects how strict the etag comparison is.
| Type | Syntax | Meaning |
|---|---|---|
| Strong |
"a3f5c9e8b1d2"
|
Byte-for-byte identical. Any change at all produces a new ETag. |
| Weak |
W/"a3f5c9e8b1d2"
|
Semantically equivalent. Two versions can share a weak ETag even if bytes differ slightly. |
A weak ETag carries the
W/
prefix. It's useful when you want to treat two versions as "the same enough" (for example, a page that only changed a timestamp comment). Weak ETags cannot be used for range requests, because partial byte ranges need exact byte matching. Strong ETags can. The comparison rule also depends on the header. If-None-Match uses weak comparison, so W/"a3f5c9e8b1d2" matches "a3f5c9e8b1d2" and the server can still answer 304. If-Match and If-Range use strong comparison, so a weak ETag never matches, and an If-Match update sent with one fails with 412.
The full rules live in MDN's ETag documentation and the formal definition is in RFC 9110.
Conditional requests and headers
Conditional requests are requests that only complete an action if a condition is met. ETags power two of the most common ones:
- If-None-Match — used for caching. "Send the file only if its ETag no longer matches this one." A match returns 304.
-
If-Match
— used for safe updates. "Only apply my PUT or PATCH if the resource still has this ETag." A mismatch returns
412 Precondition Failed, preventing you from overwriting someone else's changes.
That second use case is a quiet superpower. It solves the "lost update" problem in APIs. If two people edit the same record, the second save fails instead of silently clobbering the first, a technique sometimes called optimistic concurrency control.
GET /styles.css HTTP/1.1
Host: example.com
If-None-Match: "a3f5c9e8b1d2"
# Server replies:
HTTP/1.1 304 Not Modified
ETag: "a3f5c9e8b1d2"
ETag vs Last-Modified
ETags aren't the only way to do
cache revalidation. The older
Last-Modified
header pairs with
If-Modified-Since
to do a similar job based on timestamps.
| Factor | ETag | Last-Modified |
|---|---|---|
| Precision | Detects any change, even sub-second | Limited to one-second resolution |
| Content awareness | Can reflect actual content | Only knows the timestamp |
| Edge case | Handles files that change then revert | Fooled if timestamp updates but content is identical |
| Overhead | May require hashing | Nearly free from filesystem metadata |
When both headers are present, ETag takes priority: the browser sends If-None-Match and If-Modified-Since together, and the server must check If-None-Match and ignore If-Modified-Since. Many servers send both so clients can use whichever they support. For most modern setups, ETags give more reliable results.
Common ETag problems and fixes
ETags are simple in theory but trip people up in a few predictable ways:
-
Mismatched ETags across servers.
Some servers (older Apache configs) build ETags from the file's inode number. On a load-balanced cluster, the same file has different inodes on each server, so ETags don't match and caching breaks. Removing the inode component (
FileETag MTime Sizeon Apache) only helps if every server has the same file timestamps; content-based hashing fixes it for good. - ETags as unintended trackers. Because ETags are unique and persistent, they can be abused to identify returning visitors even after cookies are cleared. Privacy tools and audits flag this pattern, so avoid embedding user-specific data in ETag values.
-
Compression changing the tag.
A gzipped response has different bytes from the plain file, so servers change its tag: Apache's mod_deflate adds a
-gzipsuffix, and nginx turns a strong ETag weak. If the server then fails to match that changed tag when the browser sends it back inIf-None-Match, every revalidation returns a full200instead of a304. Check with curl that a repeat request carrying the received ETag gets a304; if it doesn't, fix how the server compares the changed tag rather than turning ETags off. -
Disabling ETags unnecessarily.
Some tutorials tell you to turn ETags off. That's only sensible if your
Cache-ControlandLast-Modifiedsetup already covers you and cross-server mismatches are a problem. Otherwise you lose free revalidation.
Decode the 304 responses ETags rely on
Since ETag cache validation lives and dies by the 304 Not Modified response, our full HTTP status code guide explains what it and the other common codes signal.
Read the status code guide →
They reduce bandwidth and load time by skipping downloads of unchanged files. When a resource hasn't changed, the server sends a tiny 304 response instead of the full file. The request still happens, but the transfer is far smaller, which speeds up repeat visits noticeably.
A strong ETag means the content is byte-for-byte identical, so any change creates a new tag. A weak ETag, marked with a
W/
prefix, means two versions are semantically equivalent even if minor bytes differ. Strong ETags support range requests; weak ones do not.
Use ETags when you need precise change detection, since they catch sub-second edits and content that changes then reverts. Last-Modified is cheaper but limited to one-second timestamps. Many servers send both, and when a request carries both validators, the server checks the ETag and ignores the date.
This usually happens on load-balanced servers where ETags are built from file inode numbers, which differ per machine. On dynamic pages it also happens when the hashed output includes something that changes each time, such as a timestamp, CSRF token or nonce. Switch to content-based hashing, and leave per-request values out of what you hash.
Yes, and it's a known privacy concern. Because ETags are unique and persist in the cache, a server can assign a distinct value to each visitor and read it back even after cookies are deleted. Avoid putting user-specific data in ETag values to prevent this abuse.