ETag and Conditional Requests

Validating cache freshness with entity tags

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 Host: api.example.com
2

Server Response with ETag

Server includes ETag in response

HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-cache ETag: "abc123" {"data": "original content"}
3

Subsequent Request

Browser sends stored ETag

GET /api/data HTTP/1.1 Host: api.example.com If-None-Match: "abc123"
4a

Content Unchanged → 304

Server confirms cache is valid (no body transferred)

HTTP/1.1 304 Not Modified ETag: "abc123" (no body - browser uses cached copy)
or
4b

Content Changed → 200

Server sends new content with new ETag

HTTP/1.1 200 OK ETag: "def456" {"data": "updated content"}

Server: Implementing ETags (Node.js)

import { createServer } from 'node:http'; import { createHash } from 'node:crypto'; // Generate ETag from content function generateETag(content) { return '"' + createHash('md5').update(content).digest('hex').substring(0, 16) + '"'; } const server = createServer((req, res) => { // Get current data (in reality, from database) const data = JSON.stringify({ users: [{ id: 1, name: 'Alice' }], timestamp: Date.now() }); const etag = generateETag(data); const clientETag = req.headers['if-none-match']; // Check if client's cached version matches if (clientETag === etag) { // Content unchanged - tell client to use cache res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-cache' }); res.end(); // No body! return; } // Content changed or first request - send full response 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: Based on content hash // More accurate - detects any change 'ETag: "abc123"' 'If-None-Match: "abc123"' // Last-Modified: Based on timestamp // Less accurate - 1-second resolution 'Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT' 'If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT' // Server implementation with Last-Modified 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); // Best practice: Use both res.writeHead(200, { 'ETag': etag, 'Last-Modified': lastModified, 'Cache-Control': 'no-cache' });

Conditional Updates (PUT/PATCH)

// Prevent lost updates with If-Match // Only update if ETag matches (optimistic locking) // Client sends update with current ETag async function updateResource(id, data, etag) { const response = await fetch(`/api/items/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'If-Match': etag // Only update if this version }, body: JSON.stringify(data) }); if (response.status === 412) { // Precondition Failed - someone else updated it throw new Error('Resource was modified by another user'); } return response.json(); } // Server-side handling if (req.method === 'PUT') { const ifMatch = req.headers['if-match']; const currentETag = getCurrentETag(resourceId); if (ifMatch && ifMatch !== currentETag) { // Resource changed since client fetched it res.writeHead(412, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Precondition Failed', message: 'Resource was modified', currentETag: currentETag })); return; } // Safe to update 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.