An ETag (entity tag) is a unique identifier for a specific version of a resource.
When the content changes, the ETag changes. This allows efficient cache validation without
transferring the full response.
When Cache-Control says to revalidate, the browser sends the stored ETag
to ask "is my copy still fresh?" The server responds with either 304 (use cache) or
the new content.
Conditional Request Flow
1
First Request
Browser requests the resource
GET /api/data HTTP/1.1
api.example.com
↓
2
Server Response with ETag
Server includes ETag in response
HTTP/1.1 200 OK
application/json
no-cache
"abc123"
{"data": "original content"}
↓
3
Subsequent Request
Browser sends stored ETag
GET /api/data HTTP/1.1
api.example.com
"abc123"
↓
4a
Content Unchanged → 304
Server confirms cache is valid (no body transferred)
HTTP/1.1 304 Not Modified
"abc123"
or
4b
Content Changed → 200
Server sends new content with new ETag
HTTP/1.1 200 OK
"def456"
{"data": "updated content"}
Server: Implementing ETags (Node.js)
import { createServer } from 'node:http';
import { createHash } from 'node:crypto';
function generateETag(content) {
return '"' + createHash('md5').update(content).digest('hex').substring(0, 16) + '"';
}
const server = createServer((req, res) => {
const data = JSON.stringify({
users: [{ id: 1, name: 'Alice' }],
timestamp: Date.now()
});
const etag = generateETag(data);
const clientETag = req.headers['if-none-match'];
if (clientETag === etag) {
res.writeHead(304, {
'ETag': etag,
'Cache-Control': 'no-cache'
});
res.end();
return;
}
res.writeHead(200, {
'Content-Type': 'application/json',
'ETag': etag,
'Cache-Control': 'no-cache'
});
res.end(data);
});
server.listen(3000);
The 304 response saves bandwidth by not re-sending the body. The browser
uses its cached copy instead. This is especially useful for large responses.
ETag vs Last-Modified
'ETag: "abc123"'
'If-None-Match: "abc123"'
'Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT'
'If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT'
const lastModified = new Date(file.mtime).toUTCString();
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince && new Date(ifModifiedSince) >= new Date(lastModified)) {
res.writeHead(304);
res.end();
return;
}
res.setHeader('Last-Modified', lastModified);
res.writeHead(200, {
'ETag': etag,
'Last-Modified': lastModified,
'Cache-Control': 'no-cache'
});
Conditional Updates (PUT/PATCH)
async function updateResource(id, data, etag) {
const response = await fetch(`/api/items/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': etag
},
body: JSON.stringify(data)
});
if (response.status === 412) {
throw new Error('Resource was modified by another user');
}
return response.json();
}
if (req.method === 'PUT') {
const ifMatch = req.headers['if-match'];
const currentETag = getCurrentETag(resourceId);
if (ifMatch && ifMatch !== currentETag) {
res.writeHead(412, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Precondition Failed',
message: 'Resource was modified',
currentETag: currentETag
}));
return;
}
updateResource(resourceId, body);
}
If-Match prevents "lost update" problems when two users edit the same
resource simultaneously. The second user gets a 412 error and can fetch the latest
version before trying again.
← Back to HTTP Examples