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:

LevelDescriptionExample
Client-sideBrowser stores responseService workers, HTTP cache
CDN / EdgeResponse cached close to usersCloudflare, Fastly
Reverse ProxySits between client and serverNginx, Varnish
ApplicationCache logic in your server codeRedis, memory cache
DatabaseCaching inside DB layerPostgres 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:

HeaderPurpose
Cache-ControlTells how and how long to cache
ETagVersioning via content fingerprint
Last-ModifiedTimestamp-based versioning
ExpiresDeprecated 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 or If-Modified-Since headers

Flow:

  1. Server returns ETag: "abc123"
  2. Client stores it
  3. Next time, client sends If-None-Match: "abc123"
  4. 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 CaseStrategy
Small app, no infra overheadIn-memory caching
Multiple instances / shared dataRedis
Public GET APIs / CDN deliveryHTTP headers (Cache-Control)
Reduce bandwidth on repeat fetchETag / 304
Low-latency + freshnessStale-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.