APIs are the backbone of modern apps, but they can become a bottleneck if not optimized. Caching is one of the most effective ways to reduce load, improve response time, and save resources.
🚀 Why cache API responses?
Caching lets you store expensive responses and reuse them, avoiding repeated work. This leads to:
- ⚡ Faster response times
- 🧠 Less load on databases or third-party services
- 💵 Reduced infrastructure costs
- 🛡️ Better resilience under traffic spikes
🧩 Where can you cache?
You can cache at multiple levels:
Level | Description | Example |
Client-side | Browser stores response | Service workers, HTTP cache |
CDN / Edge | Response cached close to users | Cloudflare, Fastly |
Reverse Proxy | Sits between client and server | Nginx, Varnish |
Application | Cache logic in your server code | Redis, memory cache |
Database | Caching inside DB layer | Postgres query cache, Mongo memory cache |
Each layer has trade-offs. Let’s focus on API-level caching you control as a developer.
🧠 1. In-memory caching (per instance)
Use this when:
- The cache is small (a few MBs)
- You don’t need persistence
- You don’t horizontally scale your backend (single instance)
const cache = new Map();
function getCachedUser(id) {
if (cache.has(id)) return cache.get(id);
const user = db.users.findById(id);
cache.set(id, user);
return user;
}
Pros:
- Very fast
- No external setup
Cons:
- Not shared across instances
- Lost on restart
🔁 2. Redis caching (CENTRALIZED)
Use Redis when:
- You need shared cache across multiple server instances
- You want to expire data or implement LRU policies
- You cache database queries, computed results, or external API calls
const redis = require('ioredis')();
const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
await redis.set(key, JSON.stringify(user), 'EX', 60); // 60 sec TTL
return user;
Pros:
- Shared across all servers
- TTL, eviction, pub/sub, etc.
- Fast (in-memory)
Cons:
- Extra service to maintain
- Needs serialization (JSON/string)
📦 3. HTTP Caching with headers
Use HTTP headers when:
- You want to let clients, browsers, CDNs, or reverse proxies cache your responses
- You serve mostly read-only data
Key headers:
Header | Purpose |
Cache-Control | Tells how and how long to cache |
ETag | Versioning via content fingerprint |
Last-Modified | Timestamp-based versioning |
Expires | Deprecated in favor of Cache-Control |
Cache-Control: public, max-age=60
ETag: "v12345"
Example (express):
res.set('Cache-Control', 'public, max-age=60');
res.set('ETag', 'v12345');
res.send(data);
Pros:
- Works with browsers and CDNs
- No server logic needed
Cons:
- Only works well for GET requests
- Not flexible per user/session
🔄 4. Conditional requests (ETag / 304)
Use when:
- You want to reduce data transfer, but not serve stale data
- Clients send
If-None-Match
orIf-Modified-Since
headers
Flow:
- Server returns
ETag: "abc123"
- Client stores it
- Next time, client sends
If-None-Match: "abc123"
- If unchanged, server returns
304 Not Modified
if (req.headers['if-none-match'] === currentETag) {
res.status(304).end();
} else {
res.set('ETag', currentETag).send(data);
}
🧰 5. Stale-while-revalidate (ADVANCED)
This strategy serves stale data instantly while revalidating in the background. Great for high-performance + fresh data.
Used by:
- Next.js
fetch(..., { next: { revalidate: 60 } })
- CDNs like Vercel/Cloudflare
You can also implement it manually with Redis or custom logic.
✅ Which caching strategy should you use?
Use Case | Strategy |
Small app, no infra overhead | In-memory caching |
Multiple instances / shared data | Redis |
Public GET APIs / CDN delivery | HTTP headers (Cache-Control ) |
Reduce bandwidth on repeat fetch | ETag / 304 |
Low-latency + freshness | Stale-while-revalidate |
🧠 Final tips
- ✅ Always cache read-heavy data that doesn't change often
- ✅ Set sensible TTLs to avoid stale bugs
- ✅ Never cache user-specific or sensitive data publicly
- ✅ Monitor hit/miss rate to measure effectiveness
- ✅ Invalidate cache when underlying data changes (event-driven)
🧠 Conclusion
Caching is a powerful lever, but only when used strategically. The best approach depends on your app’s structure, traffic patterns, and consistency needs.
Whether you use Redis for shared state, HTTP headers for CDN-level performance, or in-memory for quick wins, caching can transform both performance and cost.
Cache smart. Cache what matters. And always expire what doesn’t.