ETags & Conditional Requests: Master HTTP Caching for APIs

ETags & Conditional Requests: Master HTTP Caching for APIs

3/9/2026 API Development By Tech Writers
ETagHTTP HeadersAPI CachingConditional RequestsPerformanceWeb APIBackend

Table of Contents

Introduction

Efficient APIs require smart caching strategies to deliver fast responses and minimize unnecessary network usage. One of the most powerful tools in the API developer’s toolkit is the ETag header (Entity Tag). ETags enable cache validation, reduce bandwidth consumption, and ensure data consistency between client and server.

In 2026, with the explosive growth of mobile applications and real-time APIs, understanding ETags is essential for building scalable, performant systems. This guide covers everything you need to know about implementing and using ETags effectively.

What Are ETags?

An ETag (Entity Tag) is an HTTP response header that uniquely identifies a specific version of a resource on the server. It’s essentially a fingerprint or hash of the resource’s current state.

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

{
  "id": 1,
  "name": "Wireless Headphones",
  "price": 99.99
}

Whenever the resource changes, the server generates a new ETag value. This allows clients to know whether their cached version is still valid or outdated without re-downloading the entire resource.

Key Characteristics

  • Version Identifier: Each ETag represents a unique version of a resource
  • Server-Generated: Only the server determines the ETag value
  • Immutable: For a given state of a resource, the ETag remains the same
  • Lightweight: Usually a short string (hash, MD5, timestamp, etc.)

Why Use ETags in APIs?

ETags bring significant value to modern API design:

Reduced Bandwidth Consumption

Instead of downloading the entire resource again, clients can send a conditional request. If the resource hasn’t changed, the server responds with 304 Not Modified and no response body.

GET /api/products/101
If-None-Match: "abc123def456"

HTTP/1.1 304 Not Modified

This saves substantial bandwidth, especially for large resources.

Faster Performance

By minimizing payload sizes and response times, ETags improve user experience:

  • Reduced latency for unchanged resources
  • Lower bandwidth usage = faster overall response
  • Fewer bytes transferred = quick page loads on mobile

Concurrency Control

ETags prevent conflicting updates in multi-client scenarios using the If-Match header:

PUT /api/products/101
If-Match: "abc123def456"
Content-Type: application/json

{ "price": 89.99 }

If the ETag doesn’t match (resource was updated by another client), the server returns 412 Precondition Failed, preventing overwrite conflicts.

Data Consistency

ETags ensure clients always work with the correct version of a resource, preventing stale-data issues.

How ETags Work

The ETag workflow follows a simple but powerful pattern:

Step 1: Server Generates ETag

Client makes initial request:

GET /api/users/1

Server responds with resource and ETag:

HTTP/1.1 200 OK
ETag: "user-v1-abc123"
Content-Type: application/json

{
  "id": 1,
  "name": "John Doe",
  "email": "[email protected]"
}

Step 2: Client Caches Response

The client stores both the resource data and the ETag value locally.

Step 3: Conditional Request

For subsequent requests, the client includes the ETag using the If-None-Match header:

GET /api/users/1
If-None-Match: "user-v1-abc123"

Step 4: Server Validation

The server checks if the resource has changed:

If unchanged:

HTTP/1.1 304 Not Modified

If changed:

HTTP/1.1 200 OK
ETag: "user-v1-def789"
Content-Type: application/json

{
  "id": 1,
  "name": "John Doe",
  "email": "[email protected]"
}

Strong vs Weak ETags

Strong ETags

Strong ETags are byte-for-byte representations of a resource, generated using cryptographic hashes:

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Use cases:

  • API responses where exact content matching matters
  • Resources that must be byte-identical for validation
  • Concurrency control with If-Match

Trade-off: Computationally more expensive to generate

Weak ETags

Weak ETags represent semantic equivalence rather than byte-for-byte identity, prefixed with W/:

ETag: W/"0815"

Use cases:

  • Large resources where slight formatting differences don’t matter
  • Dynamically generated content
  • Reducing server computation overhead

Advantage: Faster computation, suitable for high-traffic APIs

Limitation: Cannot be used with If-Match for concurrency control

Conditional Request Headers

If-None-Match

Sent by the client to check if a resource has changed. Server responds with 304 if the ETag matches (resource unchanged).

GET /api/products/101
If-None-Match: "abc123", "abc124"

The server can compare against multiple ETags.

If-Match

Used for safe updates. Server only processes the request if the ETag matches (resource hasn’t been modified by others).

PUT /api/products/101
If-Match: "abc123"
Content-Type: application/json

{ "stock": 50 }

Response if ETag doesn’t match:

HTTP/1.1 412 Precondition Failed

Practical Example: Product API

Here’s a complete example implementing ETag support in a Node.js/Express API:

const express = require('express');
const crypto = require('crypto');
const app = express();

// In-memory data store
const products = {
  101: {
    id: 101,
    name: "Wireless Headphones",
    price: 120,
    stock: 45
  }
};

// Generate ETag from product data
function generateETag(product) {
  const hash = crypto.createHash('md5');
  hash.update(JSON.stringify(product));
  return hash.digest('hex');
}

// GET with ETag
app.get('/api/products/:id', (req, res) => {
  const product = products[req.params.id];
  if (!product) {
    return res.status(404).json({ error: 'Not found' });
  }

  const etag = generateETag(product);
  res.set('ETag', `"${etag}"`);

  // Check If-None-Match header
  if (req.get('If-None-Match') === `"${etag}"`) {
    return res.status(304).end();
  }

  res.json(product);
});

// PUT with If-Match for safe updates
app.put('/api/products/:id', express.json(), (req, res) => {
  const product = products[req.params.id];
  if (!product) {
    return res.status(404).json({ error: 'Not found' });
  }

  const currentETag = generateETag(product);
  const ifMatch = req.get('If-Match');

  // Check concurrency
  if (ifMatch && ifMatch !== `"${currentETag}"`) {
    return res.status(412).json({
      error: 'Precondition failed',
      message: 'Resource was modified by another client'
    });
  }

  // Update product
  Object.assign(product, req.body);
  const newETag = generateETag(product);

  res.set('ETag', `"${newETag}"`);
  res.json(product);
});

app.listen(3000, () => console.log('API running on port 3000'));

ETag Use Cases in API Testing

ETags are invaluable for comprehensive API testing:

Cache Validation Testing

Verify that clients sending valid If-None-Match headers receive 304 responses without a body.

Data Update Verification

Confirm that ETag changes whenever the underlying resource is updated, signaling that cached versions are outdated.

Concurrency Testing

Validate that APIs reject updates when clients use outdated ETags, preventing data loss from race conditions.

Performance Validation

Compare payload sizes and response times with ETags enabled vs. disabled to quantify bandwidth savings.

Header Handling Tests

Verify proper implementation of If-None-Match and If-Match conditions for reliable client-server communication.

Best Practices

1. Use Strong ETags for Precision Matters

Generate strong ETags based on exact resource content when byte-level validation is critical:

const etag = crypto.createHash('sha256')
  .update(JSON.stringify(resource))
  .digest('hex');

2. Leverage Weak ETags for Large Resources

For high-traffic APIs serving large or frequently-changing resources, weak ETags reduce computation:

res.set('ETag', `W/"${resourceVersion}"`);

3. Regenerate ETags on Every Update

Always generate a new ETag when a resource changes, ensuring clients work with the latest version.

4. Support Conditional Requests

Implement both If-None-Match and If-Match headers in your API:

// Cache validation
if (req.get('If-None-Match') === currentETag) {
  res.status(304).end();
}

// Concurrency control
if (req.get('If-Match') && req.get('If-Match') !== currentETag) {
  res.status(412).json({ error: 'Conflict' });
}

5. Combine with Other Caching Headers

Use ETags alongside Cache-Control and Last-Modified for robust caching:

res.set('ETag', `"${etag}"`);
res.set('Cache-Control', 'public, max-age=3600');
res.set('Last-Modified', new Date(resource.updatedAt).toUTCString());

6. Avoid Expensive ETag Generation

Balance accuracy with performance. For frequently-accessed resources, consider:

  • Using weak ETags
  • Caching ETag values
  • Computing ETags asynchronously

7. Document ETag Behavior

In your API documentation, clearly specify:

  • Which endpoints support ETags
  • Whether ETags are strong or weak
  • Expected behavior with conditional requests

Spring Boot

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import java.security.MessageDigest;
import java.util.Base64;

@RestController
@RequestMapping("/api")
public class ResourceController {
  
  private String generateETag(String content) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    byte[] messageDigest = md.digest(content.getBytes());
    return Base64.getEncoder().encodeToString(messageDigest);
  }
  
  @GetMapping("/resource")
  public ResponseEntity<?> getResource(
      @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) 
      throws Exception {
    
    String data = "{\"id\": 1, \"name\": \"Example\"}";
    String etagValue = generateETag(data);
    
    if (ifNoneMatch != null && ifNoneMatch.equals("\"" + etagValue + "\"")) {
      return ResponseEntity.status(304).build();
    }
    
    return ResponseEntity.ok()
        .header(HttpHeaders.ETAG, "\"" + etagValue + "\"")
        .body(data);
  }
  
  @PutMapping("/resource")
  public ResponseEntity<?> updateResource(
      @RequestHeader(value = "If-Match", required = false) String ifMatch,
      @RequestBody Map<String, Object> data) throws Exception {
    
    String currentETag = generateETag(getCurrentResourceData());
    
    if (ifMatch != null && !ifMatch.equals("\"" + currentETag + "\"")) {
      return ResponseEntity.status(412)
          .body(new ErrorResponse("Precondition Failed", 
                 "Resource has been modified by another client"));
    }
    
    // Update resource
    String newETag = generateETag(data.toString());
    
    return ResponseEntity.ok()
        .header(HttpHeaders.ETAG, "\"" + newETag + "\"")
        .body(data);
  }
}

Spring Boot provides built-in ETag support through ShallowEtagHeaderFilter for automatic caching at the filter level.

Express.js

const etag = require('etag');

app.get('/api/resource', (req, res) => {
  const data = { /* ... */ };
  const etagValue = etag(JSON.stringify(data));
  
  res.set('ETag', etagValue);
  if (req.get('If-None-Match') === etagValue) {
    res.status(304).end();
  } else {
    res.json(data);
  }
});

Django

from django.views.decorators.http import condition

def resource_etag(request):
  return generate_etag_for_resource()

@condition(etag_func=resource_etag)
def get_resource(request):
  return JsonResponse(resource_data)

Django automatically handles 304 responses when ETags match.

FastAPI

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import hashlib

app = FastAPI()

def calculate_etag(data: str) -> str:
  return hashlib.md5(data.encode()).hexdigest()

@app.get("/api/resource")
async def get_resource(request: Request):
  data = {"id": 1, "name": "Example"}
  etag_value = calculate_etag(str(data))
  
  if request.headers.get("If-None-Match") == f'"{etag_value}"':
    return JSONResponse(status_code=304)
  
  response = JSONResponse(content=data)
  response.headers["ETag"] = f'"{etag_value}"'
  return response

FAQ

Q: Are ETags the same as Last-Modified headers?
A: No. Last-Modified uses timestamps, which can be imprecise. ETags provide exact content matching and are more reliable for concurrency control.

Q: Can I use both ETag and Last-Modified headers?
A: Yes. Using both provides defense in depth. Clients can use whichever method the server supports.

Q: What happens if I change my ETag generation algorithm?
A: Existing cached ETags become invalid. Clients will re-fetch the resource. Plan algorithm changes carefully to avoid performance hits.

Q: Are ETags secure?
A: ETags don’t provide security. They’re visible in HTTP responses and requests. Don’t include sensitive information in ETags; use them purely for cache validation.

Q: How do I handle ETags in distributed systems?
A: Ensure all servers generate the same ETag for identical resources. Use centralized caching or consistent hashing algorithms across your cluster.

Q: Do browser caches respect ETags?
A: Yes. Modern browsers automatically send If-None-Match headers for cached resources and respect 304 responses.

Q: What’s the performance impact of ETag generation?
A: Minimal if using weak ETags or pre-computed hashes. Strong ETags require more computation but are still negligible on modern hardware.


Have experience implementing ETags in your APIs? Share your insights and lessons learned in the comments section below! 💬