HTTP

Header

Vary

Learn how the Vary header specifies which request headers affect the response. Essential for proper cache differentiation and content negotiation.

4 min read intermediate Try in Playground

TL;DR: Tells caches which request headers affect the response, creating separate cache entries for different header values. Essential for content negotiation and CDN caching.

What is Vary?

Vary tells caches (browsers, CDNs, proxies) which request headers were used to generate the response. It’s like a recipe ingredient list - it says “this response was made using these specific request headers, so cache different versions for different header values.”

This ensures users get the right version of cached content based on their specific request characteristics.

How It Works

1. Server generates response based on request headers:

GET /api/data HTTP/1.1
Accept-Language: es
User-Agent: Mobile Browser

HTTP/1.1 200 OK
Vary: Accept-Language, User-Agent
Content-Type: application/json

{"message": "Hola", "layout": "mobile"}
```text

**2. Cache stores response** with key including `Accept-Language` and `User-Agent`.

**3. Different request gets different cached version:**

```http
GET /api/data HTTP/1.1
Accept-Language: en
User-Agent: Desktop Browser

# Cache miss - different Accept-Language value
# Server generates new response for English desktop users

Common Use Cases

Content Negotiation

Different content based on client preferences:

GET /api/users HTTP/1.1
Accept: application/json
Accept-Language: fr

HTTP/1.1 200 OK
Vary: Accept, Accept-Language
Content-Type: application/json

{"message": "Bonjour", "users": [...]}
```text

### Mobile vs Desktop

Different layouts for different devices:

```http
GET /page.html HTTP/1.1
User-Agent: Mozilla/5.0 (iPhone...)

HTTP/1.1 200 OK
Vary: User-Agent
Content-Type: text/html

<html class="mobile">...</html>

Compression Support

Different encoding based on client support:

GET /style.css HTTP/1.1
Accept-Encoding: gzip, br

HTTP/1.1 200 OK
Vary: Accept-Encoding
Content-Encoding: br

[Brotli-compressed CSS]
```text

### Authentication State

Different content for logged-in users:

```http
GET /dashboard HTTP/1.1
Authorization: Bearer token123

HTTP/1.1 200 OK
Vary: Authorization
Content-Type: text/html

<html>Welcome back, John!</html>

Multiple Headers

List multiple headers separated by commas:

Vary: Accept-Language, Accept-Encoding, User-Agent
```text

This creates separate cache entries for each combination of these header values.

## Vary: \*

Special value meaning "response varies on all headers":

```http
Vary: *

This effectively disables caching because every request is considered unique. Use sparingly!

Real-World Examples

API with Multiple Formats

GET /api/users HTTP/1.1
Accept: application/xml
Accept-Language: de

HTTP/1.1 200 OK
Vary: Accept, Accept-Language
Content-Type: application/xml

<?xml version="1.0"?>
<users>
  <message>Hallo</message>
  <user>...</user>
</users>
```text

### Responsive Images

```http
GET /hero-image HTTP/1.1
User-Agent: Mozilla/5.0 (iPhone...)
Accept: image/webp

HTTP/1.1 200 OK
Vary: User-Agent, Accept
Content-Type: image/webp

[Mobile-optimized WebP image]

Personalized Content

GET /recommendations HTTP/1.1
Cookie: userId=123; preferences=sports

HTTP/1.1 200 OK
Vary: Cookie
Content-Type: application/json

{"recommendations": ["Football News", "Basketball Scores"]}
```text

### CORS Responses

```http
GET /api/data HTTP/1.1
Origin: https://app.example.com

HTTP/1.1 200 OK
Vary: Origin
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"data": "..."}

Cache Efficiency

Good Vary Usage

Limited, predictable header values:

Vary: Accept-Language  # Usually 2-5 languages
Vary: Accept-Encoding  # Usually 2-3 encodings
```text

### Problematic Vary Usage

Headers with many unique values:

```http
❌ Vary: User-Agent     # Thousands of different values
❌ Vary: Cookie         # Unique per user
❌ Vary: Authorization  # Unique per user

These create too many cache variations, reducing cache hit rates.

Best Practices

1. Only vary on headers that actually affect the response:

✅ Vary: Accept-Language  (if response changes by language)
❌ Vary: Accept-Language  (if response is always English)
```text

**2. Use specific headers, not broad ones:**

```http
✅ Vary: Accept-Encoding
❌ Vary: User-Agent  (unless truly necessary)

3. Consider cache efficiency:

# Good: 2-3 languages × 2-3 encodings = 6-9 cache entries
Vary: Accept-Language, Accept-Encoding

# Bad: Thousands of user agents × languages = too many entries
Vary: User-Agent, Accept-Language
```text

**4. Use Vary with CORS:**

```http
# When Access-Control-Allow-Origin varies by Origin
Vary: Origin

5. Document your Vary strategy:

// Cache different versions for:
// - Language (en, es, fr)
// - Encoding (gzip, br, identity)
// - Format (json, xml)
app.get('/api/data', (req, res) => {
  res.set('Vary', 'Accept-Language, Accept-Encoding, Accept')
  // ... generate response based on these headers
})
```javascript

## Server Implementation

### Express.js

```javascript
app.get('/api/users', (req, res) => {
  const lang = req.headers['accept-language'] || 'en'
  const format = req.headers['accept'] || 'application/json'

  // Set Vary header
  res.set('Vary', 'Accept-Language, Accept')

  // Generate response based on headers
  if (format.includes('xml')) {
    res.type('xml')
    res.send(generateXMLResponse(lang))
  } else {
    res.json(generateJSONResponse(lang))
  }
})

Nginx

# Vary on encoding for compressed responses
location ~* \.(css|js)$ {
    add_header Vary "Accept-Encoding";
    gzip_vary on;
}

# Vary on user agent for mobile detection
location / {
    set $mobile "";
    if ($http_user_agent ~* "(Mobile|Android|iPhone)") {
        set $mobile "mobile";
    }

    if ($mobile) {
        add_header Vary "User-Agent";
        try_files $uri $uri/mobile.html $uri/index.html;
    }
}
```text

## Common Mistakes

### Forgetting Vary with Content Negotiation

```http
Missing Vary header:
GET /api/data HTTP/1.1
Accept: application/xml

HTTP/1.1 200 OK
Content-Type: application/xml
# Missing: Vary: Accept

✅ With proper Vary:
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/xml

Over-Varying

❌ Too many variations:
Vary: User-Agent, Cookie, Authorization, X-Custom-Header

✅ Minimal necessary variations:
Vary: Accept-Language, Accept-Encoding
```text

### Inconsistent Vary Headers

```http
❌ Sometimes includes header, sometimes doesn't:
Response 1: Vary: Accept-Language
Response 2: Vary: Accept-Language, Accept-Encoding

✅ Consistent across all responses:
Always: Vary: Accept-Language, Accept-Encoding

Frequently Asked Questions

What is the Vary header?

The Vary header tells caches which request headers affect the response. Caches store different versions based on these headers, like separate cached copies for gzip and non-gzip clients.

Why use Vary: Accept-Encoding?

It tells caches to store separate versions for different encodings. Without it, a cache might serve a gzip response to a client that cannot decompress it.

What does Vary: * mean?

Vary: * means every request is unique and the response should not be cached. It effectively disables caching for that response.

How does Vary affect CDN caching?

CDNs use Vary headers to create cache keys. Too many Vary headers reduce cache hit rates. Only vary on headers that actually change the response.

Keep Learning