HTTP

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.

5 min read intermediate Try in Playground

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

  1. Open Network tab
  2. Load resource
  3. Reload page
  4. Look for 304 responses with If-None-Match headers

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).

Keep Learning