HTTP Caching: Caching at the Protocol Level for APIs and Web Pages
In this tutorial, you will learn about HTTP Caching: Caching at the Protocol Level for APIs and Web Pages. We cover key concepts, practical examples, and best practices to help you master this topic.
HTTP caching operates at the protocol level, enabling browsers, CDNs, and proxies to cache HTTP responses without application involvement. By leveraging HTTP headers correctly, you can achieve efficient caching for both web pages and API responses without writing application-level cache logic.
flowchart LR
Browser -->|Request| Proxy[Forward Proxy / CDN]
Proxy -->|Conditional Request| Origin[Origin Server]
Origin -->|200 with Cache-Control| Proxy
Proxy -->|304 with ETag match| Proxy
Proxy --> Browser
Browser -->|If-None-Match| Proxy
Proxy -->|Cache Hit| Browser
What You'll Learn
- HTTP cache headers for controlling browser and CDN caching
- Conditional requests (If-None-Match, If-Modified-Since)
- Caching REST APIs: GET, POST, and error responses
- Cache invalidation via HTTP methods (PURGE, BAN)
Why It Matters
HTTP caching is the most widely supported caching mechanism. It works across all browsers, CDNs, and proxy servers with zero application changes when configured correctly. Misconfigured HTTP caching is the most common cause of both performance problems and stale-content bugs.
Real-World Use
A SaaS application serves its React SPA with Cache-Control: public, max-age=0, must-revalidate for index.html and Cache-Control: public, max-age=31536000, immutable for JS bundles. API responses use ETags for efficient revalidation. The result: instant page loads after the first visit.
HTTP Caching for APIs
REST API with Conditional Requests
const crypto = require('crypto');
app.get('/api/users/:id', async (req, res) => {
const user = await db.getUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
const etag = crypto.createHash('md5').update(JSON.stringify(user)).digest('hex');
res.set({
'ETag': etag,
'Cache-Control': 'private, max-age=0, must-revalidate',
'Last-Modified': new Date(user.updatedAt).toUTCString()
});
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(user);
});
Expected output:
First request: 200 with user data and ETag. Second request with If-None-Match: 304 Not Modified, no body.
Caching POST Search Results
app.post('/api/search', async (req, res) => {
const searchHash = crypto.createHash('md5').update(JSON.stringify(req.body)).digest('hex');
const cacheKey = `search:${searchHash}`;
const cached = await cache.get(cacheKey);
if (cached) {
res.set('X-Cache', 'HIT');
return res.json(JSON.parse(cached));
}
const results = await searchEngine.query(req.body);
if (results.length > 0) {
await cache.setEx(cacheKey, 60, JSON.stringify(results));
}
res.set('X-Cache', 'MISS');
res.json(results);
});
Expected output:
For repeat searches with identical query bodies, results are cached for 60 seconds. X-Cache header indicates hit/miss.
PURGE Method for Cache Invalidation
app.purge('/api/cache/:path(*)', async (req, res) => {
const path = req.params.path;
const apiKey = req.headers['x-purge-key'];
if (apiKey !== process.env.PURGE_KEY) {
return res.status(403).json({ error: 'Invalid purge key' });
}
try {
// Clear application cache
await cache.del(`response:${path}`);
// Send purge request to CDN
await fetch(`https://cdn.example.com/purge/${path}`, {
method: 'PURGE',
headers: { 'X-Purge-Key': process.env.CDN_PURGE_KEY }
});
res.json({ purged: path });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
Expected output:
Sending a PURGE request to /api/cache/products invalidates both the application cache and the CDN cache for that path.
Common Mistakes
- Setting Cache-Control: no-store for all API responses, preventing any caching — use no-cache or short max-age instead.
- Not including ETags or Last-Modified for API responses, forcing re-download of unchanged data.
- Caching authenticated responses without Vary: Cookie or Vary: Authorization, leaking user-specific data.
- Using the same cache policy for 404 and 500 responses — cache 404 briefly to reduce load, but never cache 5xx.
- Forgetting that HTTP caching is per-URL — query parameters change the cache key unless normalized.
Practice Questions
- How does a conditional request reduce bandwidth?
- What is the difference between public and private Cache-Control in HTTP caching?
- How does the Vary header affect HTTP caching?
- When would you use a PURGE request vs. waiting for TTL expiry?
- How does HTTP caching differ for API responses vs. static assets?
Challenge
Design the HTTP caching Strategy for a SaaS dashboard. The HTML shell should revalidate always. JS/CSS should cache for 1 year with immutable. User settings API should use private, max-age=0 with ETags. Search API should cache POST results for 60 seconds. Implement all headers and test with curl.
FAQ
Mini Project
Build an Express server that serves both a simple HTML page and a REST API. Configure HTTP headers for: index.html (no-cache), bundle.js (1 year, immutable), /api/public (public, max-age=60), /api/private (private, max-age=0, ETag). Use curl to verify all caching behaviors including 304 responses.
What's Next
Complete the caching journey with Cache Project — a comprehensive hands-on project combining all caching strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro