Cache Headers: Cache-Control, Expires, and Validation Directives
In this tutorial, you will learn about Cache Headers: Cache. We cover key concepts, practical examples, and best practices to help you master this topic.
HTTP cache headers are the primary mechanism for controlling how browsers, CDNs, and proxy caches store and reuse responses. By setting the right directives, you define freshness, validation, and cacheability policies without writing any application code.
flowchart LR
A[Server Response] --> B{Cache-Control Present?}
B -->|Yes| C{public or private?}
C -->|public| D[CDN and Browser Cache]
C -->|private| E[Browser Cache Only]
B -->|No| F[Check Expires Header]
F -->|Has Expires| G[Use Expires Date]
F -->|No Expires| H[Heuristic Freshness]
D --> I{max-age elapsed?}
I -->|No| J[Serve Fresh Cache]
I -->|Yes| K[Revalidate with Origin]
What You'll Learn
- Every Cache-Control directive: public, private, no-cache, no-store, max-age, s-maxage, must-revalidate, proxy-revalidate
- ETag and Last-Modified validation mechanisms
- How to combine headers for optimal caching behavior
Why It Matters
Well-configured cache headers can reduce origin server load by 70-90% with zero application changes. Misconfigured headers are the most common cause of caching bugs — either serving stale content or not caching at all.
Real-World Use
A SaaS dashboard sets Cache-Control: public, max-age=0, must-revalidate on HTML pages so browsers always revalidate, but sets Cache-Control: public, max-age=31536000, immutable on versioned JS bundles so they are cached for a year without revalidation.
Validation with ETags
const crypto = require('crypto');
app.get('/api/resource', async (req, res) => {
const data = await getData();
const etag = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
if (req.headers['if-none-match'] === etag) {
res.status(304).end();
return;
}
res.set('ETag', etag);
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
res.json(data);
});
Expected output:
Browser sends If-None-Match header on repeat requests. If data unchanged, server returns 304 Not Modified with empty body.
Validation with Last-Modified
app.get('/api/articles', async (req, res) => {
const articles = await getArticles();
const lastModified = new Date(articles[0].updatedAt).toUTCString();
if (req.headers['if-modified-since'] === lastModified) {
res.status(304).end();
return;
}
res.set('Last-Modified', lastModified);
res.set('Cache-Control', 'public, max-age=0');
res.json(articles);
});
Expected output:
Browser sends If-Modified-Since. If articles haven't changed since that date, server returns 304.
Granular Cache-Control Directives for API
app.get('/api/user/:id/profile', (req, res) => {
const userId = req.params.id;
if (req.user.id !== userId) {
res.set('Cache-Control', 'public, max-age=60');
} else {
res.set('Cache-Control', 'private, max-age=300');
}
res.json(getProfile(userId));
});
Expected output:
Other users' profile pages are publicly cacheable for 60s. The user's own profile is private and cached only in the browser for 300s.
Common Mistakes
- Setting
Cache-Control: no-storeeverywhere because you don't understand caching, losing all performance benefits. - Using
max-agewithoutpublicorprivate, causing unexpected behavior in CDNs. - Forgetting to set
must-revalidatefor time-sensitive content that should never serve stale. - Setting
Expiresheader to a past date to disable caching — useCache-Control: no-cacheinstead. - Not including the
Varyheader when caching responses that differ by Accept-Encoding, Accept-Language, or Cookie.
Practice Questions
- What is the difference between
no-cacheandno-store? - How does
s-maxagediffer frommax-age? - What is a conditional request and which headers enable it?
- Why would you set
Cache-Control: immutableon a JavaScript bundle? - What does the
Vary: Accept-Encodingheader tell a CDN?
Challenge
Design a caching Strategy for a multi-language website. Use the Vary header to cache separate versions per language. Set different Cache-Control policies for the homepage (revalidate always), article pages (stale while revalidate 1 hour), and API data (max-age 60s).
FAQ
Mini Project
Create an Express server with three endpoints configured with different cache policies: /static/* with max-age=31536000 and immutable, /api/public with max-age=60 and stale-while-revalidate=300, /api/user with private max-age=0. Use curl to verify the Cache-Control headers and conditional request behavior with If-None-Match.
What's Next
Continue with ETags for a deep dive into entity tag validation and strong vs. weak comparison.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro