Skip to content

ETags: Entity Tags for Cache Validation and Concurrency Control

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about ETags: Entity Tags for Cache Validation and Concurrency Control. We cover key concepts, practical examples, and best practices to help you master this topic.

An ETag (entity tag) is an HTTP response header that acts as a unique identifier for a specific version of a resource. Clients send the ETag back via the If-None-Match (cache validation) or If-Match (concurrency control) headers, enabling efficient Caching and preventing lost updates.

flowchart TB
    C[Client] -->|GET /resource| S[Server]
    S -->|200 + ETag: abc123| C
    C -->|GET /resource
If-None-Match: abc123| S S -->|304 Not Modified| C C -->|PUT /resource
If-Match: abc123| S S -->|200 OK + ETag: def456| C C -->|PUT /resource
If-Match: obsolete| S S -->|412 Precondition Failed| C

What You'll Learn

  • Strong vs. weak ETags and when to use each
  • ETag generation strategies: content hashing, version numbers, timestamps
  • Using ETags for optimistic concurrency control in REST APIs
  • Combining ETags with Cache-Control for efficient caching

Why It Matters

ETags are the most precise cache validation mechanism — they detect byte-level changes that Last-Modified timestamps miss. For APIs, ETags prevent the lost update problem where two clients overwrite each other's changes.

Real-World Use

A document editing API generates strong ETags by hashing the document content. When a client saves changes, it sends If-Match with the ETag. If another client has already saved, the server returns 412 Precondition Failed, and the client must refresh before overwriting.

Strong vs. Weak ETags

Strong ETag (Content Hash)

const crypto = require('crypto');

function generateStrongETag(content) {
  return '"' + crypto.createHash('sha256').update(JSON.stringify(content)).digest('hex') + '"';
}

app.get('/api/documents/:id', async (req, res) => {
  const doc = await db.getDocument(req.params.id);
  const etag = generateStrongETag(doc);

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.set('ETag', etag);
  res.json(doc);
});

Expected output:

ETag: "abc123def456..." — any byte change in the document produces a different hash.

Weak ETag (Semantic Equivalence)

function generateWeakETag(timestamp) {
  return 'W/"' + timestamp.getTime().toString(36) + '"';
}

app.get('/api/posts', async (req, res) => {
  const posts = await db.getPosts();
  const lastModified = posts.reduce((max, p) => Math.max(max, new Date(p.updatedAt).getTime()), 0);
  const etag = generateWeakETag(new Date(lastModified));

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.set('ETag', etag);
  res.json(posts);
});

Expected output:

ETag: W/"abc123" — semantically equivalent representations share the same weak ETag even if serialization differs slightly.

Optimistic Concurrency with PUT

app.put('/api/documents/:id', async (req, res) => {
  const existing = await db.getDocument(req.params.id);
  const currentEtag = generateStrongETag(existing);

  if (req.headers['if-match'] !== currentEtag) {
    return res.status(412).json({ error: 'Precondition Failed: document has been modified' });
  }

  const updated = await db.updateDocument(req.params.id, req.body);
  res.set('ETag', generateStrongETag(updated));
  res.json(updated);
});

Expected output:

If document was modified by another client, PUT with If-Match old ETag returns 412 and the client must re-GET.

Common Mistakes

  • Using weak ETags for byte-level cache validation — weak ETags allow semantically equivalent but byte-different responses.
  • Generating ETags with too-expensive hashing (e.g., reading entire file into memory for large blobs).
  • Returning ETags without Cache-Control headers, so browsers don't use them for conditional requests.
  • Not supporting If-None-Match for GET and If-Match for PUT/PATCH/DELETE uniformly.
  • Using ETag as a security mechanism — ETags reveal version information but don't authenticate.

Practice Questions

  1. What is the difference between a strong and weak ETag?
  2. How does an ETag-based conditional request reduce bandwidth?
  3. Why would a server return 412 Precondition Failed?
  4. Can two different resources share the same ETag?
  5. How does ETag validation differ from Last-Modified validation in precision?

Challenge

Design an API for a collaborative markdown editor. Use ETags for both caching (GET) and concurrency (PUT). When a conflict is detected (412), return the current document version and the diff so the client can merge.

FAQ

What is the difference between If-None-Match and If-Match?

If-None-Match is used for cache validation (GET) — return 304 if ETag matches. If-Match is used for concurrency (PUT/PATCH) — only proceed if ETag matches, otherwise 412.

Can ETags be used for security?

No. ETags are for caching and concurrency, not authentication or authorization. They reveal version info but shouldn't be treated as secrets.

What is a good ETag generation strategy?

For JSON APIs, hash the serialized response body. For files, use inode + mtime + size. For database rows, use row version or updated_at timestamp.

Should I include ETag in every response?

Ideally yes, especially for GET responses that are cacheable. It costs minimal computation and enables efficient revalidation.

How long should an ETag be valid?

ETags are valid until the resource changes. They have no expiration — the server replaces them on modification.

Mini Project

Extend the blog API from earlier lessons. Add ETag support to all GET endpoints using SHA-256 hashing. Implement conditional request handling. Add PUT /posts/:id with If-Match concurrency control. Write a client script that demonstrates the 412 conflict scenario.

What's Next

Continue with Cache Control to master advanced Cache-Control directives for APIs, SPAs, and static assets.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro