ETags & Conditional Requests: Master HTTP Caching for APIs
Table of Contents
- Introduction
- What Are ETags?
- Why Use ETags in APIs?
- How ETags Work
- Strong vs Weak ETags
- Conditional Request Headers
- Practical Example: Product API
- ETag Use Cases in API Testing
- Best Practices
- ETags in Popular Frameworks
- FAQ
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
ETags in Popular Frameworks
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.
Related Articles
- API Development Best Practices — Building Secure & Scalable APIs
- Web Performance Optimization — Speed Up Your Applications
- Database Design Fundamentals — Building Efficient Databases
- REST API Architecture — Understanding Modern Web APIs
Have experience implementing ETags in your APIs? Share your insights and lessons learned in the comments section below! 💬