Why Your API Strategy Needs Caching
A single API request often triggers a heavy chain of events. Your server might spend 200ms querying a database, processing business logic, and serializing JSON. If the data hasn’t changed since the last request, that effort is wasted. I’ve seen simple header tweaks save companies thousands in monthly cloud egress costs by preventing redundant data transfers.
Caching is essentially about avoiding unnecessary work. By serving a stored copy of a resource, you can drop your server load significantly for read-heavy apps. This isn’t just a performance boost. It provides a vital safety net that keeps your infrastructure alive during unexpected traffic spikes.
The Core Pillars: Freshness and Validation
You don’t need Redis or a complex infrastructure to start caching. The HTTP protocol has these features baked in. To implement it correctly, you only need to master two concepts: Freshness and Validation.
Freshness headers tell the client how long a resource remains valid. Think of the Cache-Control header as an expiration date. Validation headers, like ETag, act as a checksum. They allow the client to ask, “I have version X of this data; do I really need to download it again?”
In modern environments like Node.js or Go, you rarely “install” caching. Instead, you configure middleware to inject these headers into your responses. Here is a standard Express endpoint before we add any optimization:
const express = require('express');
const app = express();
app.get('/api/products', (req, res) => {
const products = [{ id: 1, name: 'Laptop' }, { id: 2, name: 'Phone' }];
res.json(products);
});
app.listen(3000, () => console.log('Server running on port 3000'));
Choosing the Right Cache-Control Policy
The Cache-Control header is your primary lever for performance. It is a multi-directive instruction for browsers and CDNs. Misconfiguring this is a common source of “stale data” bugs, so precision matters.
Public vs. Private
Generic data, such as a list of 50 public blog posts, should be marked as public. This allows CDNs to store the response and serve it to thousands of users. Conversely, user-specific data like a billing dashboard must be private. This ensures only the specific user’s browser stores the data, keeping sensitive information off shared intermediate servers.
Setting the max-age
The max-age directive defines the Time To Live (TTL) in seconds. For a product catalog that updates once an hour, use 3600. For static assets that never change, you might go as high as a year (31536000):
Cache-Control: public, max-age=3600
The Difference Between No-Cache and No-Store
- no-cache: This is a bit of a misnomer. It tells the browser it can store the data, but it must check with the server before using it. This is perfect when you want the speed of ETags without the risk of showing outdated info.
- no-store: Use this for high-security endpoints, like a password reset token. It forbids the browser and all proxies from saving any copy of the data.
Saving Bandwidth with ETags
ETags (Entity Tags) handle the validation side of the equation. An ETag is a unique string—often an MD5 hash—representing the current state of a resource. When the data changes, the hash changes.
The workflow is straightforward. First, the server sends a response with an ETag: "v123". On the next request, the client sends that back in an If-None-Match: "v123" header. If the data is identical, the server returns a 304 Not Modified status. The response body is empty, which saves massive amounts of bandwidth on large JSON payloads.
const crypto = require('crypto');
app.get('/api/data', (req, res) => {
const data = { message: "Hello World", timestamp: "2023-10-27" };
const jsonString = JSON.stringify(data);
const etag = crypto.createHash('md5').update(jsonString).digest('hex');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, no-cache');
res.send(data);
});
While frameworks often automate this, manual implementation helps you spot bugs. For instance, if your JSON includes a “generated_at” timestamp, your ETag will change every time, rendering the cache useless.
Verifying Your Implementation
Never assume your cache is working just because you wrote the code. Open your browser’s Network tab and check the “Size” column. You want to see “(from disk cache)” or a status of “304”.
Testing with cURL
I prefer curl for quick header verification. Use the -I flag to view only the headers without the clutter of the body:
curl -I http://localhost:3000/api/data
To simulate a returning user, pass the ETag back to the server:
curl -I -H 'If-None-Match: "your-hash-here"' http://localhost:3000/api/data
A successful setup will return HTTP/1.1 304 Not Modified.
Watch Out for the Vary Header
A common pitfall is ignoring the Vary header. If your API serves different content based on the Authorization or Accept-Language headers, you must tell the cache. Otherwise, User A might accidentally see a cached version of User B’s data.
Vary: Authorization, Accept-Encoding
Mastering these headers turns a fragile API into a robust, scalable system. It is a high-impact optimization that requires minimal code but delivers significant performance gains as your user base grows.

