- Home
- HTTP Headers
- If-None-Match Header
Header
If-None-Match Header
Learn how the If-None-Match header makes conditional requests using ETags. Avoid downloading unchanged resources and reduce bandwidth with cache validation.
TL;DR: Makes conditional requests using ETags for precise cache validation. Server returns 304 Not Modified if ETags match, avoiding unnecessary downloads of unchanged resources.
What is If-None-Match?
The If-None-Match header makes conditional requests using ETags (entity tags). It’s like asking “only send me this resource if its fingerprint has changed.” ETags are more precise than modification dates and work better for dynamic content.
This header is crucial for efficient caching and preventing unnecessary data transfers.
How If-None-Match Works
1. Client requests with ETag condition:
GET /api/user/42 HTTP/1.1
Host: example.com
If-None-Match: "abc123def456"
```text
**2a. Resource unchanged (304 response):**
```http
HTTP/1.1 304 Not Modified
ETag: "abc123def456"
Cache-Control: max-age=3600
2b. Resource changed (200 response):
HTTP/1.1 200 OK
ETag: "xyz789ghi012"
Content-Type: application/json
{"id": 42, "name": "Jane Doe", "updatedAt": "2026-01-18T10:30:00Z"}
```text
## ETag Formats
### Strong ETags
```http
If-None-Match: "abc123def456"
If-None-Match: "v2.1.0-build-1234"
Weak ETags
If-None-Match: W/"abc123def456"
```text
### Multiple ETags
```http
If-None-Match: "abc123", "def456", "ghi789"
Wildcard (Any ETag)
If-None-Match: *
```text
## Real-World Examples
### API Resource Caching
```http
GET /api/posts/15 HTTP/1.1
Host: blog.example.com
If-None-Match: "post-15-v3"
Accept: application/json
Web Page Caching
GET /dashboard HTTP/1.1
Host: app.example.com
If-None-Match: "page-dashboard-abc123"
Accept: text/html
```text
### Image Caching
```http
GET /profile-pic.jpg HTTP/1.1
Host: cdn.example.com
If-None-Match: "img-42-hash-xyz789"
Preventing Lost Updates
PUT /api/posts/15 HTTP/1.1
Host: blog.example.com
If-None-Match: "post-15-v2"
Content-Type: application/json
{"title": "Updated Title", "content": "..."}
```text
## Server Response Patterns
### Not Modified (304)
```http
HTTP/1.1 304 Not Modified
ETag: "abc123def456"
Cache-Control: max-age=1800
Vary: Accept-Encoding
Modified (200)
HTTP/1.1 200 OK
ETag: "xyz789ghi012"
Content-Type: application/json
Last-Modified: Thu, 18 Jan 2026 14:20:00 GMT
{"data": "updated content"}
```text
### Precondition Failed (412)
```http
HTTP/1.1 412 Precondition Failed
ETag: "current-etag-value"
Content-Type: application/json
{"error": "Resource has been modified by another request"}
Client Implementation
Caching with ETags
class ETagCache {
constructor() {
this.cache = new Map()
}
async fetch(url) {
const cached = this.cache.get(url)
const headers = {}
if (cached?.etag) {
headers['If-None-Match'] = cached.etag
}
const response = await fetch(url, { headers })
if (response.status === 304) {
// Use cached data
return cached.data
} else if (response.ok) {
// Update cache
const data = await response.json()
const etag = response.headers.get('ETag')
this.cache.set(url, { data, etag })
return data
}
throw new Error(`Request failed: ${response.status}`)
}
}
// Usage
const cache = new ETagCache()
const userData = await cache.fetch('/api/user/42')
```javascript
### Optimistic Updates
```javascript
async function updateResource(id, data, currentETag) {
const response = await fetch(`/api/resources/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': currentETag // Ensure we're updating the right version
},
body: JSON.stringify(data)
})
if (response.status === 412) {
throw new Error('Resource was modified by another user')
}
return response.json()
}
Server Implementation
Express.js with ETags
app.get('/api/posts/:id', (req, res) => {
const post = getPost(req.params.id)
if (!post) return res.status(404).json({ error: 'Not found' })
// Generate ETag based on content
const etag = generateETag(post)
// Check If-None-Match
const ifNoneMatch = req.headers['if-none-match']
if (ifNoneMatch) {
const clientETags = ifNoneMatch.split(',').map((tag) => tag.trim())
if (clientETags.includes(etag) || clientETags.includes('*')) {
return res.status(304).set('ETag', etag).end()
}
}
// Send updated resource
res.set('ETag', etag)
res.json(post)
})
function generateETag(data) {
const crypto = require('crypto')
const content = JSON.stringify(data)
return `"${crypto.createHash('md5').update(content).digest('hex')}"`
}
```javascript
### Preventing Lost Updates
```javascript
app.put('/api/posts/:id', (req, res) => {
const post = getPost(req.params.id)
if (!post) return res.status(404).json({ error: 'Not found' })
const currentETag = generateETag(post)
const ifMatch = req.headers['if-match']
// Check if client has the latest version
if (ifMatch && ifMatch !== currentETag) {
return res.status(412).set('ETag', currentETag).json({
error: 'Resource has been modified',
currentVersion: currentETag
})
}
// Update the resource
const updatedPost = updatePost(req.params.id, req.body)
const newETag = generateETag(updatedPost)
res.set('ETag', newETag)
res.json(updatedPost)
})
ETag Generation Strategies
Content-Based (Strong)
function generateContentETag(data) {
const crypto = require('crypto')
const content = JSON.stringify(data)
return `"${crypto.createHash('sha256').update(content).digest('hex')}"`
}
```javascript
### Version-Based
```javascript
function generateVersionETag(resource) {
return `"v${resource.version}-${resource.id}"`
}
Timestamp-Based (Weak)
function generateTimestampETag(resource) {
const timestamp = new Date(resource.updatedAt).getTime()
return `W/"${resource.id}-${timestamp}"`
}
```javascript
## Best Practices
### Use Strong ETags for Critical Data
```javascript
// ✅ Strong ETag for financial data
const etag = `"${crypto.createHash('sha256').update(JSON.stringify(transaction)).digest('hex')}"`
// ❌ Weak ETag for critical updates
const etag = `W/"${transaction.id}-${Date.now()}"`
Handle Multiple ETags
function parseIfNoneMatch(header) {
if (!header) return []
return header
.split(',')
.map((tag) => tag.trim())
.map((tag) => tag.replace(/^W\//, '')) // Remove weak indicator
}
```text
### Combine with Cache-Control
```javascript
res.set({
ETag: etag,
'Cache-Control': 'max-age=300, must-revalidate',
Vary: 'Accept-Encoding'
})
Performance Benefits
Bandwidth Savings
API response without ETag caching:
- Every request: 50KB response
- 100 requests: 5MB total
With ETag caching (90% cache hit rate):
- 10 requests: 50KB each = 500KB
- 90 requests: 304 responses = ~18KB
- Total: ~518KB (90% savings!)
Reduced Server Load
- Skip database queries for unchanged resources
- Avoid expensive computations
- Reduce JSON serialization overhead
Testing ETags
Using curl
# First request (get ETag)
curl -v https://api.example.com/posts/1
# Conditional request
curl -H 'If-None-Match: "abc123def456"' \
https://api.example.com/posts/1
# Multiple ETags
curl -H 'If-None-Match: "old-etag", "another-etag"' \
https://api.example.com/posts/1
Browser DevTools
- Open Network tab
- Load resource
- Reload page
- Look for 304 responses with If-None-Match headers
Related Headers
- ETag - Server’s entity tag
- If-Match - Conditional updates with ETags
- If-Modified-Since - Date-based conditional requests
- Cache-Control - Caching directives
Weak vs Strong ETags and When to Use Each
ETags come in two forms: strong and weak. A strong ETag (e.g., "abc123") guarantees byte-for-byte identity — two responses with the same strong ETag are identical in every byte. A weak ETag (e.g., W/"abc123") indicates semantic equivalence — the responses represent the same content but may differ in minor ways like whitespace, compression, or header order.
Strong ETags are required for range requests. If a client downloads part of a file and wants to resume, it sends If-Range with the ETag. The server must use a strong ETag to guarantee the file has not changed between the partial downloads. Weak ETags cannot be used with If-Range.
For API responses, weak ETags are often more practical. A JSON response might be semantically identical even if the key order changes or whitespace differs between serializations. Using a weak ETag based on the underlying data version (e.g., a database row’s updated_at timestamp) is simpler to implement and avoids false cache misses from cosmetic differences in serialization.
Frequently Asked Questions
What is If-None-Match?
If-None-Match is a conditional request header that sends cached ETags to the server. If any ETag matches, the server returns 304 Not Modified instead of the full response.
How does If-None-Match work?
The client sends ETags from cached responses. The server compares them with the current ETag. If matched, it returns 304; if not, it returns 200 with new content.
Can If-None-Match contain multiple ETags?
Yes, you can send multiple ETags comma-separated: If-None-Match: "abc", "def". The server returns 304 if any match. Use * to match any existing resource.
What is the difference between If-None-Match and If-Match?
If-None-Match succeeds when ETags do NOT match (for GET caching). If-Match succeeds when ETags DO match (for PUT/DELETE to prevent lost updates).