HTTP

Header

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.

7 min read beginner Try in Playground

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/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"}
```text

**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):

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

## 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

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

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

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

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

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"}
```text

### 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

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>
```javascript

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

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}
```javascript

### 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

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;
    }
}
```text

## Best Practices

### 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

// 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())
```text

**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

// 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())
```text

**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

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
}
```javascript

**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

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
}
```text

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

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

- 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

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

### Medium-Lived Data

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

Long-Lived Data

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

### Immutable Assets

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

No Caching

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

## 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

// 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)
}
  • Cache-Control - Modern caching directives (takes precedence)
  • Date - When the response was generated
  • Age - How old the cached response is
  • Last-Modified - When resource was last changed

Frequently Asked Questions

What is the Expires header?

The Expires header specifies an absolute date/time after which the response is considered stale. Browsers will not use cached content after this time without revalidation.

Should I use Expires or Cache-Control?

Use Cache-Control max-age instead. It is more flexible and takes precedence over Expires. Expires requires absolute dates which can cause issues with clock skew.

What format does Expires use?

Expires uses HTTP-date format: "Expires: Wed, 21 Oct 2026 07:28:00 GMT". The date must be in GMT timezone. Invalid dates are treated as already expired.

How do I set Expires to never cache?

Set Expires to a past date like "Expires: 0" or "Expires: Thu, 01 Jan 1970 00:00:00 GMT". However, Cache-Control: no-store is the preferred modern approach.

Keep Learning