Skip to main content
โšก Calmops

API Caching Strategies: Complete Guide

API Caching Strategies: Complete Guide

Caching is essential for building high-performance APIs. This guide covers everything from HTTP caching headers to distributed caching with Redis and CDN integration.

Why API Caching Matters

  • Reduces Latency: Serve cached responses instantly
  • Decreases Server Load: Fewer requests hit your backend
  • Improves Scalability: Handle more traffic with same resources
  • Cost Reduction: Lower cloud infrastructure costs

HTTP Caching Headers

Cache-Control

# No caching
Cache-Control: no-store
Cache-Control: no-cache, must-revalidate

# Cache with expiration
Cache-Control: max-age=3600
Cache-Control: max-age=3600, public

# Shared caching (proxies)
Cache-Control: s-maxage=7200
Cache-Control: s-maxage=3600, proxy-revalidate

# Validation
Cache-Control: must-revalidate
Cache-Control: stale-while-revalidate=600
Cache-Control: stale-if-error=86400

Implementation

// Express.js - Set cache headers
app.get('/api/users', (req, res) => {
  // Cache for 5 minutes, shared for 10 minutes
  res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
  
  // Or disable caching
  res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
  
  res.json(users);
});

// Cache for different response types
app.get('/api/users/:id', (req, res) => {
  if (req.headers['if-none-match']) {
    // Client has cached version, check ETag
    const etag = generateETag(user);
    if (req.headers['if-none-match'] === etag) {
      return res.status(304).end(); // Not Modified
    }
  }
  
  res.set('ETag', generateETag(user));
  res.set('Cache-Control', 'private, max-age=300');
  res.json(user);
});

ETag (Entity Tags)

import hashlib
import json
from functools import wraps

def generate_etag(data):
    """Generate ETag from data"""
    content = json.dumps(data, sort_keys=True)
    return f'"{hashlib.md5(content.encode()).hexdigest()}"'

def etag_middleware(app):
    @app.route('/api/resource')
    def get_resource():
        data = fetch_data()
        etag = generate_etag(data)
        
        # Check If-None-Match header
        if request.headers.get('If-None-Match') == etag:
            return '', 304
        
        response = jsonify(data)
        response.headers['ETag'] = etag
        response.headers['Cache-Control'] = 'max-age=0, must-revalidate'
        return response
    
    return app

Last-Modified & If-Modified-Since

// Express.js with Last-Modified
app.get('/api/users', (req, res) => {
  const users = fetchUsers();
  const lastModified = new Date(Math.max(...users.map(u => new Date(u.updatedAt))));
  
  // Check if client has newer version
  const ifModifiedSince = req.headers['if-modified-since'];
  if (ifModifiedSince) {
    const clientDate = new Date(ifModifiedSince);
    if (clientDate >= lastModified) {
      return res.status(304).end();
    }
  }
  
  res.set('Last-Modified', lastModified.toUTCString());
  res.set('Cache-Control', 'public, max-age=300');
  res.json(users);
});

Vary Header

# Cache different versions based on headers
Vary: Accept-Encoding
Vary: Accept-Language
Vary: User-Agent
Vary: Cookie
// Implementation
app.get('/api/data', (req, res) => {
  // Different users see different data
  res.set('Vary', 'Cookie, Authorization');
  
  // Or vary by client
  res.set('Vary', 'Accept, Accept-Encoding');
  
  res.json(data);
});

Application-Level Caching

In-Memory Caching

// Node.js with node-cache
const NodeCache = require('node-cache');

const cache = new NodeCache({
  stdTTL: 300, // 5 minutes default
  checkperiod: 60, // Check every 60 seconds
  useClones: false // Use references for performance
});

// Get or fetch
async function getUsers() {
  const cacheKey = 'users:list';
  let users = cache.get(cacheKey);
  
  if (!users) {
    users = await db.users.findAll();
    cache.set(cacheKey, users, 300);
  }
  
  return users;
}

// With async/await wrapper
const getOrSet = async (key, fn, ttl = 300) => {
  const cached = cache.get(key);
  if (cached) return cached;
  
  const result = await fn();
  cache.set(key, result, ttl);
  return result;
};

// Usage
app.get('/api/users', async (req, res) => {
  const users = await getOrSet('users:list', () => db.users.findAll());
  res.json(users);
});

Redis Caching

const redis = require('redis');
const client = redis.createClient();

client.on('error', (err) => console.error('Redis error:', err));

// Basic operations
async function cacheGet(key) {
  const data = await client.get(key);
  return data ? JSON.parse(data) : null;
}

async function cacheSet(key, value, ttlSeconds = 300) {
  await client.setex(key, ttlSeconds, JSON.stringify(value));
}

async function cacheDelete(key) {
  await client.del(key);
}

// Pattern: Cache-aside (Lazy Loading)
async function getUser(userId) {
  const cacheKey = `user:${userId}`;
  
  // Check cache first
  let user = await cacheGet(cacheKey);
  if (user) {
    return user;
  }
  
  // Fetch from database
  user = await db.users.findById(userId);
  
  // Store in cache
  if (user) {
    await cacheSet(cacheKey, user, 600); // 10 minutes
  }
  
  return user;
}

// Pattern: Write-through
async function updateUser(userId, data) {
  // Update database first
  const user = await db.users.update(userId, data);
  
  // Then update cache
  await cacheSet(`user:${userId}`, user, 600);
  
  return user;
}

Cache Invalidation Strategies

Types of Invalidation

// 1. Time-based (TTL)
await cacheSet('users:list', users, 300); // 5 minutes

// 2. Event-based (On write)
async function onUserUpdate(userId) {
  await cacheDelete(`user:${userId}`);
  await cacheDelete('users:list');
  await cacheDelete('users:recent');
}

// 3. Tag-based
async function invalidateByTag(tag) {
  const keys = await client.keys(`*:${tag}:*`);
  if (keys.length) {
    await client.del(...keys);
  }
}

// Tag example
await cacheSet('user:123:profile', user, 300, ['user:123', 'profiles']);
await cacheSet('users:list', users, 300, ['users', 'lists']);

Advanced Patterns

# Python with cache patterns
from functools import wraps
import hashlib
import json

class Cache:
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def invalidate_pattern(self, pattern):
        keys = self.redis.keys(pattern)
        if keys:
            self.redis.delete(*keys)
    
    def user_updated(self, user_id):
        """Invalidate all caches related to a user"""
        self.invalidate_pattern(f"user:{user_id}:*")
        self.invalidate_pattern("users:list:*")
        self.invalidate_pattern("users:count")

cache = Cache(redis_client)

# Use in repository
class UserRepository:
    def update(self, user_id, data):
        user = self.db.update(user_id, data)
        cache.user_updated(user_id)  # Invalidate caches
        return user

CDN Caching

Setting Up CDN Headers

// CloudFront-compatible headers
app.get('/api/assets/*', (req, res) => {
  // Cache in CDN for 1 hour, in browser for 5 minutes
  res.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
  
  // Versioned assets can be cached long-term
  res.set('Cache-Control', 'public, max-age=31536000, immutable');
  
  res.sendFile(req.params.path);
});

// API responses - shorter cache
app.get('/api/config', (req, res) => {
  res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
  res.json(config);
});

Cache-Key Strategies

# Default: URL only
# GET /api/users โ†’ cached

# With query params
# GET /api/users?page=2 โ†’ separate cache

# With vary
# GET /api/data with Accept: application/json โ†’ one version
# GET /api/data with Accept: application/xml โ†’ different version

CDN Integration Example

// Fastly
app.use((req, res, next) => {
  res.set('Surrogate-Control', 'max-age=3600');
  res.set('Cache-Tag', 'users,api,authenticated');
  next();
});

// CloudFront
app.use((req, res, next) => {
  res.set('CloudFront-Is-Desktop-Viewer', 'true');
  res.set('CloudFront-Is-Mobile-Viewer', 'false');
  res.set('CloudFront-Forwarded-Proto', 'https');
  next();
});

API-Specific Caching Strategies

Response Caching

// Cache expensive API responses
const cache = new Map();

function cacheResponse(key, data, ttl) {
  cache.set(key, {
    data,
    expires: Date.now() + ttl
  });
}

function getCachedResponse(key) {
  const cached = cache.get(key);
  if (cached && cached.expires > Date.now()) {
    return cached.data;
  }
  cache.delete(key);
  return null;
}

// Middleware
app.get('/api/search', (req, res) => {
  const cacheKey = `search:${JSON.stringify(req.query)}`;
  
  const cached = getCachedResponse(cacheKey);
  if (cached) {
    return res.set('X-Cache', 'HIT').json(cached);
  }
  
  const results = performSearch(req.query);
  cacheResponse(cacheKey, results, 60000); // 1 minute
  
  res.set('X-Cache', 'MISS').json(results);
});

Query Result Caching

// SQL query caching
const queryCache = new Map();

async function executeQuery(sql, params) {
  const cacheKey = JSON.stringify({ sql, params });
  
  // Check cache
  const cached = queryCache.get(cacheKey);
  if (cached && Date.now() - cached.timestamp < 60000) {
    return cached.result;
  }
  
  // Execute query
  const result = await db.query(sql, params);
  
  // Cache result
  queryCache.set(cacheKey, {
    result,
    timestamp: Date.now()
  });
  
  return result;
}

Caching Best Practices

Cache Hierarchy

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           CDN (Edge Cache)               โ”‚
โ”‚     Static assets, public API data       โ”‚
โ”‚           (minutes to hours)             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         Application Cache               โ”‚
โ”‚      (Redis, Memcached)                โ”‚
โ”‚        (seconds to minutes)             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚            Database                     โ”‚
โ”‚         (Primary source)                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Decision Matrix

Data Type Cache Location TTL Invalidation
Static assets CDN Long (1 year) Version URL
User public profile Redis + CDN Medium (1 hour) On update
User private data Browser Short (5 min) On update
Search results Redis Short (1 min) Time-based
Session data Redis Session Logout
API rate limits Redis Real-time Auto

Common Mistakes

// Bad: No cache key
cache.set('users', users);

// Good: Specific cache keys
cache.set('users:list:page=1:sort=name', users);
cache.set(`user:${userId}:profile`, user);

// Bad: No TTL
cache.set('data', data);

// Good: Always set TTL
cache.set('data', data, 300);

// Bad: Caching user-specific data in shared cache
cache.set('users', users); // All users see same cache!

// Good: User-specific keys
cache.set(`users:${userId}`, userData);

Performance Monitoring

// Track cache hit rate
const cacheStats = { hits: 0, misses: 0 };

function trackCache(operation, key) {
  if (operation === 'hit') cacheStats.hits++;
  if (operation === 'miss') cacheStats.misses++;
  
  const total = cacheStats.hits + cacheStats.misses;
  const hitRate = total > 0 ? (cacheStats.hits / total * 100).toFixed(2) : 0;
  
  console.log(`Cache ${operation}: ${key}`);
  console.log(`Hit rate: ${hitRate}%`);
}

// Expose metrics
app.get('/metrics/cache', (req, res) => {
  const total = cacheStats.hits + cacheStats.misses;
  res.json({
    hits: cacheStats.hits,
    misses: cacheStats.misses,
    hitRate: total > 0 ? (cacheStats.hits / total) : 0
  });
});

External Resources


Comments