# Expires Header

> Learn how the Expires header specifies when cached responses become stale. Understand date formats and when to use Expires vs Cache-Control for caching.

Source: https://howhttpworks.com/headers/expires
Last reviewed: 2026-10-04

> **TL;DR:** Sets an absolute expiration date for cached content using HTTP-date format. Largely superseded by Cache-Control max-age but still used for legacy compatibility.

## What is Expires?

The **Expires** header specifies a date and time after which the response is considered stale. It's like a "best before" date on food packaging, telling caches when the content should no longer be used without revalidation.

This is an older caching mechanism, now largely superseded by Cache-Control max-age, but still widely supported.

## How Expires Works

**Server sets expiration time:**

```http
HTTP/1.1 200 OK
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: Sat, 18 Jan 2026 11:00:00 GMT
Content-Type: application/json

{"data": "This is fresh for 1 hour"}
```

**Cache determines if content is fresh:**

- Before expiration time: Use cached version
- After expiration time: Revalidate with server

## Syntax

```http
Expires: <http-date>
```

### Format

Uses HTTP-date format (same as Date header):

```http
Expires: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
```

## Common Examples

### 1 Hour Cache

```http
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: Sat, 18 Jan 2026 11:00:00 GMT
```

Content expires 1 hour after creation.

### 1 Day Cache

```http
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: Sun, 19 Jan 2026 10:00:00 GMT
```

Content fresh for 24 hours.

### Already Expired (No Cache)

```http
Expires: Sat, 18 Jan 2026 09:00:00 GMT
```

Date in the past means "don't cache."

### Far Future Expiration

```http
Expires: Wed, 18 Jan 2036 10:00:00 GMT
```

Cache for 10 years (for immutable resources).

## Real-World Scenarios

### Static Assets

```http
GET /assets/logo.png HTTP/1.1
Host: cdn.example.com

HTTP/1.1 200 OK
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: Wed, 18 Jan 2036 10:00:00 GMT
Cache-Control: public, immutable
Content-Type: image/png

[Image data]
```

### API Response

```http
GET /api/config HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: Sat, 18 Jan 2026 10:15:00 GMT
Cache-Control: public, max-age=900

{"theme": "dark", "language": "en"}
```

### No Caching

```http
GET /api/account HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: 0
Cache-Control: no-cache, no-store

{"balance": 1000, "lastLogin": "2026-01-18T10:00:00Z"}
```

### RSS Feed

```http
GET /feed.xml HTTP/1.1
Host: blog.example.com

HTTP/1.1 200 OK
Date: Sat, 18 Jan 2026 10:00:00 GMT
Expires: Sat, 18 Jan 2026 11:00:00 GMT

<?xml version="1.0"?>
<rss>...</rss>
```

## Server Implementation

### Express.js (Node.js)

```javascript
const express = require('express')
const app = express()

// Set expiration 1 hour in the future
app.get('/api/data', (req, res) => {
  const now = new Date()
  const expires = new Date(now.getTime() + 60 * 60 * 1000) // +1 hour

  res.setHeader('Date', now.toUTCString())
  res.setHeader('Expires', expires.toUTCString())
  res.setHeader('Cache-Control', 'public, max-age=3600')

  res.json({ data: 'example' })
})

// Static assets - far future expiration
app.get('/assets/*', (req, res, next) => {
  const now = new Date()
  const expires = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000) // +1 year

  res.setHeader('Expires', expires.toUTCString())
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')

  next()
})

// No caching for sensitive data
app.get('/api/account', (req, res) => {
  const now = new Date()
  const expires = new Date(0) // Unix epoch (always expired)

  res.setHeader('Expires', expires.toUTCString())
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')

  res.json({ balance: 1000 })
})

// Helper function for relative expiration
function setExpires(res, seconds) {
  const expires = new Date(Date.now() + seconds * 1000)
  res.setHeader('Expires', expires.toUTCString())
}

app.get('/cached', (req, res) => {
  setExpires(res, 3600) // 1 hour
  res.json({ cached: true })
})
```

### FastAPI (Python)

```python
from fastapi import FastAPI, Response
from datetime import datetime, timedelta
from email.utils import formatdate
import time

app = FastAPI()

def format_http_date(dt):
    """Convert datetime to HTTP date format"""
    timestamp = time.mktime(dt.timetuple())
    return formatdate(timeval=timestamp, localtime=False, usegmt=True)

@app.get("/api/data")
async def get_data(response: Response):
    now = datetime.utcnow()
    expires = now + timedelta(hours=1)

    response.headers["Date"] = format_http_date(now)
    response.headers["Expires"] = format_http_date(expires)
    response.headers["Cache-Control"] = "public, max-age=3600"

    return {"data": "example"}

@app.get("/assets/{path:path}")
async def static_assets(response: Response):
    now = datetime.utcnow()
    expires = now + timedelta(days=365)

    response.headers["Expires"] = format_http_date(expires)
    response.headers["Cache-Control"] = "public, max-age=31536000, immutable"

    return {"asset": "data"}

@app.get("/api/account")
async def account(response: Response):
    # Set to epoch (already expired)
    epoch = datetime(1970, 1, 1)

    response.headers["Expires"] = format_http_date(epoch)
    response.headers["Cache-Control"] = "no-cache, no-store"

    return {"balance": 1000}
```

### Django

```python
from django.http import JsonResponse
from django.utils.http import http_date
from datetime import datetime, timedelta
import time

def data_view(request):
    now = datetime.utcnow()
    expires = now + timedelta(hours=1)

    response = JsonResponse({'data': 'example'})
    response['Date'] = http_date()
    response['Expires'] = http_date(time.mktime(expires.timetuple()))
    response['Cache-Control'] = 'public, max-age=3600'

    return response

def static_asset_view(request):
    expires = datetime.utcnow() + timedelta(days=365)

    response = JsonResponse({'asset': 'data'})
    response['Expires'] = http_date(time.mktime(expires.timetuple()))
    response['Cache-Control'] = 'public, max-age=31536000, immutable'

    return response

def no_cache_view(request):
    response = JsonResponse({'balance': 1000})
    response['Expires'] = http_date(0)  # Epoch
    response['Cache-Control'] = 'no-cache, no-store'

    return response
```

### Nginx

```nginx
server {
    listen 80;
    server_name example.com;

    # Static assets - 1 year expiration
    location /assets/ {
        root /var/www;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # API - 15 minutes
    location /api/config {
        expires 15m;
        add_header Cache-Control "public, max-age=900";
        proxy_pass http://backend;
    }

    # No caching
    location /api/account {
        expires epoch;
        add_header Cache-Control "no-cache, no-store";
        proxy_pass http://backend;
    }

    # Custom expiration time
    location /data/ {
        expires modified +1h;  # 1 hour after last modification
        proxy_pass http://backend;
    }
}
```

## Getting Expires right

### For Servers

**1. Use Cache-Control instead of Expires**

```http
# ✅ Modern approach (Cache-Control takes precedence)
Cache-Control: max-age=3600
Expires: Sat, 18 Jan 2026 11:00:00 GMT

# ⚠️ Old approach (still works but less flexible)
Expires: Sat, 18 Jan 2026 11:00:00 GMT
```

**2. Include both for compatibility**

```javascript
// Support both old and new clients
const now = new Date()
const expires = new Date(now.getTime() + 3600 * 1000)

res.setHeader('Cache-Control', 'public, max-age=3600')
res.setHeader('Expires', expires.toUTCString())
```

**3. Use epoch for no-cache**

```http
# ✅ Clear "don't cache" signal
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store
```

**4. Set reasonable expiration times**

```javascript
// Static assets - long cache
res.setHeader('Expires', new Date(Date.now() + 31536000000).toUTCString())

// API data - short cache
res.setHeader('Expires', new Date(Date.now() + 300000).toUTCString())

// User data - no cache
res.setHeader('Expires', new Date(0).toUTCString())
```

**5. Keep server clocks synchronized**

```bash
# Expires is absolute time, so accurate clocks are critical
sudo timedatectl set-ntp true
```

### For Clients

**1. Prefer Cache-Control over Expires**

```javascript
const cacheControl = response.headers.get('Cache-Control')
const expires = response.headers.get('Expires')

// Cache-Control takes precedence if both present
if (cacheControl) {
  // Use max-age from Cache-Control
} else if (expires) {
  // Fall back to Expires
}
```

**2. Handle clock skew**

```javascript
const serverDate = new Date(response.headers.get('Date'))
const expires = new Date(response.headers.get('Expires'))
const clientNow = new Date()

// Calculate freshness relative to server time, not client time
const maxAge = (expires - serverDate) / 1000
const age = (clientNow - serverDate) / 1000
const remainingFresh = maxAge - age
```

**3. Check for expired content**

```javascript
function isExpired(response) {
  const expires = response.headers.get('Expires')
  if (!expires) return false

  const expiresDate = new Date(expires)
  const now = new Date()

  return now > expiresDate
}
```

## Expires vs Cache-Control

### Cache-Control Takes Precedence

```http
# If both are present, Cache-Control max-age wins
Cache-Control: max-age=7200
Expires: Sat, 18 Jan 2026 11:00:00 GMT

# The response will be cached for 2 hours (7200 seconds)
# regardless of what Expires says
```

### When to Use Each

**Use Cache-Control (Modern)**

```http
Cache-Control: max-age=3600, public
```

- More flexible
- Relative time (not affected by clock skew)
- More directives available

**Use Expires (Legacy Support)**

```http
Expires: Sat, 18 Jan 2026 11:00:00 GMT
```

- Support old HTTP/1.0 clients
- Simple absolute time
- Always include Cache-Control too

## Common Expiration Patterns

### Short-Lived Data

```http
Expires: Sat, 18 Jan 2026 10:05:00 GMT  # 5 minutes
Cache-Control: max-age=300
```

### Medium-Lived Data

```http
Expires: Sat, 18 Jan 2026 11:00:00 GMT  # 1 hour
Cache-Control: max-age=3600
```

### Long-Lived Data

```http
Expires: Sun, 19 Jan 2026 10:00:00 GMT  # 1 day
Cache-Control: max-age=86400
```

### Immutable Assets

```http
Expires: Wed, 18 Jan 2036 10:00:00 GMT  # 10 years
Cache-Control: max-age=31536000, immutable
```

### No Caching

```http
Expires: Thu, 01 Jan 1970 00:00:00 GMT  # Epoch
Cache-Control: no-cache, no-store
```

## Testing Expires Header

### Using curl

```bash
# Check Expires header
curl -I https://example.com

# Check with verbose output
curl -v https://example.com

# Extract only Expires
curl -s -I https://example.com | grep -i "^Expires:"
```

### Using JavaScript

```javascript
// Check expiration
fetch('https://api.example.com/data').then((response) => {
  const expires = response.headers.get('Expires')
  const date = response.headers.get('Date')

  if (expires) {
    const expiresDate = new Date(expires)
    const serverDate = new Date(date)
    const freshFor = (expiresDate - serverDate) / 1000

    console.log('Expires:', expiresDate)
    console.log('Fresh for:', freshFor, 'seconds')
  }
})

// Calculate remaining freshness
function getRemainingFreshness(response) {
  const expires = new Date(response.headers.get('Expires'))
  const now = new Date()

  return Math.max(0, (expires - now) / 1000)
}
```

## Related Headers

- [Cache-Control](https://howhttpworks.com/headers/cache-control) - Modern caching directives (takes precedence)
- [Date](https://howhttpworks.com/headers/date) - When the response was generated
- [Age](https://howhttpworks.com/headers/age) - How old the cached response is
- [Last-Modified](https://howhttpworks.com/headers/last-modified) - When resource was last changed
