Node.js Performance with Redis Caching
From 800ms API responses to 12ms — a practical caching guide
The problem
An endpoint that aggregated user analytics was taking ~800ms on average. Not slow enough to panic, but slow enough for users to notice. After profiling, the culprit was clear: redundant database queries for data that barely changed.
Why Redis
Redis is an in-memory data store — reads are in the microsecond range compared to milliseconds for database queries. For data that:
- Changes infrequently (less than once per minute)
- Is expensive to compute (multiple database joins)
- Is requested by many users
Redis caching is almost always the right answer.
The implementation
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function getAnalytics(userId: string) {
const cacheKey = `analytics:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const data = await db.query(`
SELECT ... FROM analytics
WHERE user_id = $1
GROUP BY ...
`, [userId]);
await redis.setEx(cacheKey, 300, JSON.stringify(data));
return data;
}
Cache invalidation
Phil Karlton's famous quote says there are only two hard things in Computer Science: cache invalidation and naming things.
He wasn't wrong. For this use case, I used TTL-based expiry (5 minutes) rather than explicit invalidation. The trade-off:
- TTL expiry: Simple and self-healing. Users may see up to 5 minutes of stale data.
- Explicit invalidation: Zero stale data. More complex — you must invalidate on every write.
async function updateAnalyticsEvent(userId: string, event: Event) {
await db.insert(event);
await redis.del(`analytics:${userId}`);
}
Results
| Metric | Before | After |
|---|---|---|
| Average response time | 820ms | 12ms |
| P95 response time | 1,400ms | 18ms |
| DB queries per minute | 4,200 | 380 |
The 98% reduction in response time also reduced database load significantly — a useful secondary benefit.
Don't cache everything
Only cache data you can tolerate being stale. User-facing financial data, for example, should never be cached without very careful consideration.
Cache invalidation bugs can be subtle and severe. Start with data that genuinely does not need to be real-time.