- Home
- HTTP Headers
- Expires Header
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.
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)
}
Related Headers
- 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.