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.
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.
// 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.
// 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();
}