DevKits
Concept

HTTP Expires Header — What It Is, How It Works & When to Use

The Expires response header tells browsers and CDNs a wall-clock deadline after which a resource is stale. Learn the exact HTTP-date format, four production-ready configurations (Apache, Nginx, Express, CDN), how Expires interacts with Cache-Control, and why max-age replaced it as the preferred caching directive.

Last updated:

Core Concepts

The HTTP-date format (RFC 7231)
Expires values must follow the strict HTTP-date format: `Expires: Thu, 01 Dec 2025 16:00:00 GMT`. The day name, comma, day-of-month, month abbreviation, year, time in HH:MM:SS, and the literal string 'GMT' are all required. Any deviation — a missing comma, lowercase 'gmt', a two-digit year — makes the date invalid per RFC 9111, and caches may ignore the header entirely. Common expiry values: 1 hour, 30 days, 1 year (for immutable resources), and special values like `Expires: 0` (already expired) or `Expires: -1` (don't cache at all).
Expires vs Cache-Control: max-age
When both `Expires` and `Cache-Control: max-age` are present, max-age always wins (RFC 9111 §5.3). max-age is relative (e.g. 3600 seconds from the response Date), so it's immune to server clock drift. Expires is absolute (e.g. 'Thu, 01 Dec 2025') which means a clock 5 minutes slow makes the resource appear 5 minutes fresher than it actually is. For new deployments, always prefer max-age; use Expires only as a legacy fallback for older CDNs that don't understand Cache-Control.
When to use Expires (and when not to)
Yes: as a compatibility fallback alongside Cache-Control for CDN interop; for .htaccess-based static sites where you can't easily inject Cache-Control; as a debug signal (temporary Expires: -1 disables caching without restarting anything). No: as the sole caching directive on new services — if you expire at the wrong time there's no fallback; for resources you want to cache forever — use `Cache-Control: public, max-age=31536000, immutable` instead; for authenticated responses — these should use `Cache-Control: private` or `no-store`, never a public Expires date.
Common pitfalls with clock drift and CDNs
Every server in your CDN edge network has its own clock. If edge-A's clock is 30s ahead of edge-B, the same resource appears to expire 30s earlier on edge-A — this causes inconsistent cache behaviour across regions. NTP (Network Time Protocol) mitigates this but never eliminates it entirely. For CDN-cached content, prefer max-age (relative) and add `s-maxage` for CDN-specific control. Also: Expires timestamps are in GMT — always; a local timezone timestamp will be parsed incorrectly by browsers east of UTC.

Frequently Asked Questions

What should I use instead of Expires?

Cache-Control: max-age for nearly all new server configurations. It's relative (no clock dependency), gives finer control (public/private, s-maxage, must-revalidate, no-cache, no-store, immutable), and is supported by every browser and CDN released since 2010. If you need backward compatibility, include Expires as a fallback — but never as the sole mechanism.

How do I set the Expires header on Nginx / Apache / Express?

Nginx: `expires 30d;` in a location block sets max-age and auto-generates the Expires date. Apache: `ExpiresDefault 'access plus 1 month'` in .htaccess or httpd.conf (requires mod_expires). Express: `res.set('Expires', new Date(Date.now() + 30*24*60*60*1000).toUTCString());`. CDNs: Cloudflare 'Browser Cache TTL' setting controls both; Akamai uses a property called 'Downstream Cache'. Each of our /http-headers/expires examples shows the exact syntax.

Try these related tools