Response header
< Expires: Wed, 21 Oct 2026 07:28:00 GMTExpires 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.
- Direction
- Response
- Category
- Caching
- Spec
- RFC 9111 §5.3
On this page
- What is Expires?
- How Expires Works
- Syntax
- Format
- Common Examples
- 1 Hour Cache
- 1 Day Cache
- Already Expired (No Cache)
- Far Future Expiration
- Real-World Scenarios
- Static Assets
- API Response
- No Caching
- RSS Feed
- Server Implementation
- Express.js (Node.js)
- FastAPI (Python)
- Django
- Nginx
- Getting Expires right
- For Servers
- For Clients
- Expires vs Cache-Control
- Cache-Control Takes Precedence
- When to Use Each
- Common Expiration Patterns
- Short-Lived Data
- Medium-Lived Data
- Long-Lived Data
- Immutable Assets
- No Caching
- Testing Expires Header
- Using curl
- Using JavaScript
- Related Headers
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"}
Cache determines if content is fresh:
- Before expiration time: Use cached version
- After expiration time: Revalidate with server
Syntax
Expires: <http-date>
Format
Uses HTTP-date format (same as Date header):
Expires: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
Common Examples
1 Hour Cache
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
Content fresh for 24 hours.
Already Expired (No Cache)
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
Cache for 10 years (for immutable resources).
Real-World Scenarios
Static Assets
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"}
No Caching
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>
Server Implementation
Express.js (Node.js)
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}
Django
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;
}
}
Getting Expires right
For Servers
1. Use Cache-Control instead of Expires
# ✅ 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())
3. Use epoch for no-cache
# ✅ 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())
5. Keep server clocks synchronized
# 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
}
2. Handle clock skew
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
}
Expires vs Cache-Control
Cache-Control Takes Precedence
# 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
- More flexible
- Relative time (not affected by clock skew)
- More directives available
Use Expires (Legacy Support)
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
Medium-Lived Data
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
Immutable Assets
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
Testing Expires Header
Using curl
# 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: Thu, 21 Oct 2027 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.
Sources
Related
Age Header
Learn how the Age header indicates how long a response has been cached in seconds. Understand cache freshness calculations and CDN behavior.
ETag Header: Strong vs Weak, If-None-Match and 304 Debugging
ETag identifies a resource version for cache validation and optimistic locking. Strong vs weak ETags, If-None-Match, If-Match, and why nginx gzip weakens them.
Last-Modified
Learn how the Last-Modified header indicates when a resource was last changed. Enable efficient cache validation with If-Modified-Since conditional requests.
Vary Header: Cache Keys, Vary: Origin and Accept-Encoding
Vary lists the request headers a response depends on, so caches keep separate copies. Vary: Accept-Encoding, Vary: Origin for CORS, Vary: *, and CDN cache keys.