Database queries take milliseconds. Memory lookups take microseconds. Caching is the process of storing the result of expensive operations in high-speed RAM (like Redis) so subsequent requests can be served instantly.


Step 1 — What is Redis?

Redis (Remote Dictionary Server) is an open-source, in-memory key-value data store. Because it stores data in RAM rather than on a hard disk, it delivers sub-millisecond response times.

Common Redis Use Cases

  • Database Query Caching: Storing the results of complex SQL queries.
  • Session Store: Storing JWT blacklists or stateful user sessions.
  • Rate Limiting: Tracking how many API requests an IP has made.
  • Pub/Sub & Queues: Acting as a message broker between microservices.

Step 2 — The Cache-Aside Pattern

The most common caching pattern is 'Cache-Aside'. The application first asks the cache. If the data is missing (Cache Miss), it asks the database, stores the result in the cache, and returns it.

cache-aside.jsjavascript
const redis = require('redis');
const client = redis.createClient();

async function getUserProfile(userId) {
  const cacheKey = `user:profile:${userId}`;
  
  // 1. Check Cache
  const cachedData = await client.get(cacheKey);
  if (cachedData) {
    return JSON.parse(cachedData); // Cache Hit! Return instantly.
  }
  
  // 2. Cache Miss: Fetch from DB
  const user = await db.query('SELECT * FROM users WHERE id = ?', userId);
  
  // 3. Save to Cache with an Expiration Time (TTL)
  // Ex: Set ex(piration) to 3600 seconds (1 hour)
  await client.set(cacheKey, JSON.stringify(user), { EX: 3600 });
  
  return user;
}

Step 3 — Cache Invalidation

As the famous quote goes: 'There are only two hard things in Computer Science: cache invalidation and naming things.' If the database changes, the cache must be updated, otherwise users see stale data.

Invalidation Strategies

  • TTL (Time To Live): The easiest. Data simply expires after X minutes.
  • Write-Through: When you UPDATE the database, you synchronously update the cache.
  • Event-Driven: The database triggers an event (e.g. Postgres LISTEN/NOTIFY or a message queue) that tells the cache server to delete the specific key.
invalidation.jsjavascript
// Write-through example
async function updateUserProfile(userId, newData) {
  // 1. Update Database
  await db.query('UPDATE users SET ? WHERE id = ?', [newData, userId]);
  
  // 2. Invalidate Cache so the next GET request fetches fresh data
  await client.del(`user:profile:${userId}`);
}

Step 4 — Rate Limiting with Redis

Because Redis is incredibly fast and supports atomic increments, it's perfect for rate-limiting APIs to prevent abuse.

rate-limit.jsjavascript
// A simple sliding window rate limiter
async function rateLimitMiddleware(req, res, next) {
  const ip = req.ip;
  const key = `rate_limit:${ip}`;
  
  // INCR is atomic. If key doesn't exist, it sets it to 1.
  const requests = await client.incr(key);
  
  if (requests === 1) {
    // First request: set expiration to 60 seconds
    await client.expire(key, 60);
  }
  
  if (requests > 100) {
    return res.status(429).json({ error: 'Too Many Requests. Try again in a minute.' });
  }
  
  next();
}