Caching & Performance - 1/2
The Performance Nightmare That Kills User Engagement
Picture this catastrophe: Your social media platform just got featured on a major tech blog. Traffic spikes from 1,000 to 100,000 concurrent users in 30 minutes. Your database starts choking:
- Homepage load time jumps from 200ms to 15 seconds
- User profile pages timeout after 30 seconds
- Your database CPU hits 100% and stays there
- Redis memory usage spikes to 95%
- Users start abandoning your app en masse
- Your server costs skyrocket as you desperately add more instances
Meanwhile, your competitors are handling similar traffic with sub-second response times and a fraction of your infrastructure costs.
The worst part? Your code isn’t broken. Your architecture isn’t fundamentally flawed. You simply never learned the performance optimization patterns that separate amateur systems from production-grade applications.
The Uncomfortable Truth About Performance
Here’s what every senior developer learns the hard way: Performance isn’t something you optimize later—it’s an architectural decision you make from day one.
Most developers build applications like this:
- Write the feature
- Make it work
- Deploy to production
- Wait for performance complaints
- Panic and add more servers
But high-performance systems are built differently:
- Understand the performance requirements
- Design for the expected load
- Implement caching from the start
- Monitor and optimize continuously
- Scale intelligently, not desperately
The difference between these approaches isn’t just performance—it’s the difference between systems that scale gracefully and systems that collapse under their own weight.
Ready to build applications that perform like they were designed by Netflix instead of your mom’s basement server? Let’s dive into the world of intelligent caching and performance optimization.
Caching Strategies: The Art of Being Lazy (Efficiently)
Understanding Cache Hierarchies
Caching isn’t just “put Redis in front of your database.” It’s a multi-layered strategy that mirrors how your CPU handles memory.
// Poor caching strategy - everything hits the database
class BadUserService {
async getUser(userId: string): Promise<User> {
// Every request = database query (expensive!)
return await this.database.query("SELECT * FROM users WHERE id = $1", [
userId,
]);
}
async getUserPosts(userId: string): Promise<Post[]> {
// Another database hit for every request
return await this.database.query(
"SELECT * FROM posts WHERE user_id = $1 ORDER BY created_at DESC",
[userId]
);
}
async getUserProfile(userId: string): Promise<UserProfile> {
// Multiple queries for a single profile view
const user = await this.getUser(userId);
const posts = await this.getUserPosts(userId);
const followers = await this.getUserFollowers(userId);
const following = await this.getUserFollowing(userId);
return { user, posts, followers, following };
}
}
// Smart multi-layer caching strategy
class OptimizedUserService {
private memoryCache: Map<string, any> = new Map();
private redisCache: Redis;
private database: Database;
// Cache hierarchy: Memory → Redis → Database
async getUser(userId: string): Promise<User> {
const cacheKey = `user:${userId}`;
// Layer 1: In-memory cache (fastest, ~1ns access time)
if (this.memoryCache.has(cacheKey)) {
this.metrics.increment("cache.memory.hit");
return this.memoryCache.get(cacheKey);
}
// Layer 2: Redis cache (fast, ~1ms access time)
const cachedUser = await this.redisCache.get(cacheKey);
if (cachedUser) {
const user = JSON.parse(cachedUser);
// Populate memory cache for next request
this.memoryCache.set(cacheKey, user);
this.scheduleMemoryCacheEviction(cacheKey, 300000); // 5 minutes
this.metrics.increment("cache.redis.hit");
return user;
}
// Layer 3: Database (slow, ~50-200ms access time)
const user = await this.database.query(
"SELECT * FROM users WHERE id = $1",
[userId]
);
if (user) {
// Populate all cache layers
await this.redisCache.setex(cacheKey, 3600, JSON.stringify(user)); // 1 hour
this.memoryCache.set(cacheKey, user);
this.scheduleMemoryCacheEviction(cacheKey, 300000);
}
this.metrics.increment("cache.database.hit");
return user;
}
async getUserProfile(userId: string): Promise<UserProfile> {
const profileKey = `profile:${userId}`;
// Try to get the complete profile from cache first
const cachedProfile = await this.redisCache.get(profileKey);
if (cachedProfile) {
return JSON.parse(cachedProfile);
}
// If not cached, build profile and cache the result
const [user, posts, followers, following] = await Promise.all([
this.getUser(userId),
this.getUserPosts(userId),
this.getUserFollowers(userId),
this.getUserFollowing(userId),
]);
const profile = { user, posts, followers, following };
// Cache the complete profile for faster subsequent requests
await this.redisCache.setex(
profileKey,
1800, // 30 minutes (shorter TTL for composite data)
JSON.stringify(profile)
);
return profile;
}
}
The Cache Pattern Playbook
Different scenarios require different caching strategies. Here’s your decision matrix:
// Cache-Aside Pattern (Lazy Loading)
class CacheAsideService {
async getData(key: string): Promise<any> {
// Check cache first
let data = await this.cache.get(key);
if (!data) {
// Cache miss - fetch from source
data = await this.database.get(key);
if (data) {
// Populate cache for next time
await this.cache.set(key, data, TTL);
}
}
return data;
}
async updateData(key: string, newData: any): Promise<void> {
// Update database first
await this.database.update(key, newData);
// Invalidate cache (let next read repopulate)
await this.cache.delete(key);
}
}
// Write-Through Pattern (Immediate Consistency)
class WriteThroughService {
async updateData(key: string, newData: any): Promise<void> {
// Write to both database and cache simultaneously
await Promise.all([
this.database.update(key, newData),
this.cache.set(key, newData, TTL),
]);
}
async getData(key: string): Promise<any> {
// Cache should always have the data
let data = await this.cache.get(key);
if (!data) {
// Cache miss shouldn't happen, but handle it
data = await this.database.get(key);
await this.cache.set(key, data, TTL);
}
return data;
}
}
// Write-Behind Pattern (Performance Over Consistency)
class WriteBehindService {
private writeQueue: Map<string, any> = new Map();
async updateData(key: string, newData: any): Promise<void> {
// Update cache immediately
await this.cache.set(key, newData, TTL);
// Queue database write for later
this.writeQueue.set(key, newData);
// Return immediately (async database update)
}
private async processPendingWrites(): Promise<void> {
const writes = Array.from(this.writeQueue.entries());
this.writeQueue.clear();
// Batch write to database
await this.database.batchUpdate(writes);
}
constructor() {
// Process queued writes every 5 seconds
setInterval(() => this.processPendingWrites(), 5000);
}
}
// Read-Through Pattern (Cache as Primary Interface)
class ReadThroughService {
async getData(key: string): Promise<any> {
return await this.cache.getWithFallback(key, async () => {
// This function only called on cache miss
return await this.database.get(key);
});
}
}
When to use each pattern:
- Cache-Aside: Default choice for most applications
- Write-Through: When you need immediate consistency
- Write-Behind: When you need maximum write performance
- Read-Through: When cache is your primary data interface
In-Memory Caching: Redis vs. Memcached vs. Application Cache
Redis: The Swiss Army Knife of Caching
Redis isn’t just a cache—it’s a data structure server that can replace multiple tools.
// Advanced Redis caching patterns
class RedisOptimizedService {
private redis: Redis;
// String caching with compression for large objects
async cacheUserData(userId: string, userData: any): Promise<void> {
const key = `user:${userId}`;
const serialized = JSON.stringify(userData);
// Compress large objects to save memory
const compressed =
serialized.length > 1000 ? await this.compress(serialized) : serialized;
await this.redis.setex(key, 3600, compressed);
}
// Hash operations for partial updates
async cacheUserProfile(userId: string, profile: UserProfile): Promise<void> {
const key = `profile:${userId}`;
// Store as hash for efficient partial updates
await this.redis.hset(key, {
name: profile.name,
email: profile.email,
lastLogin: profile.lastLogin.toISOString(),
settings: JSON.stringify(profile.settings),
});
await this.redis.expire(key, 7200); // 2 hours
}
async updateUserLastLogin(userId: string, loginTime: Date): Promise<void> {
const key = `profile:${userId}`;
// Update only one field without fetching entire object
await this.redis.hset(key, "lastLogin", loginTime.toISOString());
}
// List operations for feeds and timelines
async cacheUserFeed(userId: string, posts: Post[]): Promise<void> {
const key = `feed:${userId}`;
// Clear existing feed
await this.redis.del(key);
// Add posts as list (ordered by time)
const postIds = posts.map((post) => post.id);
if (postIds.length > 0) {
await this.redis.lpush(key, ...postIds);
// Keep only last 100 posts in cache
await this.redis.ltrim(key, 0, 99);
// Expire feed cache after 1 hour
await this.redis.expire(key, 3600);
}
}
async addPostToUserFeed(userId: string, postId: string): Promise<void> {
const key = `feed:${userId}`;
// Add new post to beginning of feed
await this.redis.lpush(key, postId);
// Maintain feed size limit
await this.redis.ltrim(key, 0, 99);
}
// Set operations for relationships and permissions
async cacheUserPermissions(
userId: string,
permissions: string[]
): Promise<void> {
const key = `permissions:${userId}`;
// Store as set for O(1) membership testing
if (permissions.length > 0) {
await this.redis.sadd(key, ...permissions);
await this.redis.expire(key, 1800); // 30 minutes
}
}
async hasPermission(userId: string, permission: string): Promise<boolean> {
const key = `permissions:${userId}`;
return await this.redis.sismember(key, permission);
}
// Sorted sets for leaderboards and rankings
async updateUserScore(userId: string, score: number): Promise<void> {
await this.redis.zadd("leaderboard", score, userId);
}
async getTopUsers(
limit: number = 10
): Promise<Array<{ userId: string; score: number }>> {
const results = await this.redis.zrevrangebyscore(
"leaderboard",
"+inf",
"-inf",
"WITHSCORES",
"LIMIT",
0,
limit
);
const users = [];
for (let i = 0; i < results.length; i += 2) {
users.push({
userId: results[i],
score: parseFloat(results[i + 1]),
});
}
return users;
}
// Pub/Sub for real-time cache invalidation
async setupCacheInvalidation(): Promise<void> {
const subscriber = this.redis.duplicate();
await subscriber.subscribe("cache:invalidate");
subscriber.on("message", async (channel, message) => {
const { pattern, keys } = JSON.parse(message);
if (pattern === "user:*") {
// Invalidate all user-related caches
const userKeys = await this.redis.keys("user:*");
if (userKeys.length > 0) {
await this.redis.del(...userKeys);
}
} else if (keys) {
// Invalidate specific keys
await this.redis.del(...keys);
}
});
}
async invalidateUserCaches(userId: string): Promise<void> {
await this.redis.publish(
"cache:invalidate",
JSON.stringify({
keys: [
`user:${userId}`,
`profile:${userId}`,
`feed:${userId}`,
`permissions:${userId}`,
],
})
);
}
}
Memcached: Simple, Fast, Reliable
// Memcached for simple string-based caching
class MemcachedService {
private memcached: Memcached;
constructor() {
this.memcached = new Memcached(
["127.0.0.1:11211", "127.0.0.1:11212", "127.0.0.1:11213"],
{
// Connection pooling
maxConnections: 10,
// Failover configuration
failures: 5,
retry: 5000,
failuresTimeout: 30000,
// Performance tuning
reconnect: 18000000,
timeout: 5000,
idle: 5000,
}
);
}
// Consistent hashing for distributed caching
private getKey(baseKey: string): string {
// Add prefix for namespacing
return `myapp:v1:${baseKey}`;
}
async get(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.memcached.get(this.getKey(key), (err, data) => {
if (err) {
reject(err);
} else {
resolve(data ? JSON.parse(data) : null);
}
});
});
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
return new Promise((resolve, reject) => {
this.memcached.set(
this.getKey(key),
JSON.stringify(value),
ttl,
(err) => {
if (err) reject(err);
else resolve();
}
);
});
}
// Multi-get for batch operations
async getMultiple(keys: string[]): Promise<Record<string, any>> {
const prefixedKeys = keys.map((key) => this.getKey(key));
return new Promise((resolve, reject) => {
this.memcached.getMulti(prefixedKeys, (err, data) => {
if (err) {
reject(err);
} else {
const result: Record<string, any> = {};
for (const [prefixedKey, value] of Object.entries(data)) {
const originalKey = prefixedKey.replace("myapp:v1:", "");
result[originalKey] = JSON.parse(value as string);
}
resolve(result);
}
});
});
}
}
Application-Level Caching
// In-memory caching with intelligent eviction
class InMemoryCache {
private cache: Map<string, CacheEntry> = new Map();
private maxSize: number;
private ttlMs: number;
constructor(maxSize: number = 1000, ttlMs: number = 300000) {
this.maxSize = maxSize;
this.ttlMs = ttlMs;
// Cleanup expired entries every minute
setInterval(() => this.cleanup(), 60000);
}
set(key: string, value: any, customTTL?: number): void {
// Evict if cache is full
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
this.evictLRU();
}
const entry: CacheEntry = {
value,
timestamp: Date.now(),
accessCount: 0,
lastAccessed: Date.now(),
ttl: customTTL || this.ttlMs,
};
this.cache.set(key, entry);
}
get(key: string): any {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Check if expired
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
// Update access statistics
entry.accessCount++;
entry.lastAccessed = Date.now();
return entry.value;
}
// LRU (Least Recently Used) eviction
private evictLRU(): void {
let oldestKey: string | null = null;
let oldestTime = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (entry.lastAccessed < oldestTime) {
oldestTime = entry.lastAccessed;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
// Cleanup expired entries
private cleanup(): void {
const now = Date.now();
const expired: string[] = [];
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
expired.push(key);
}
}
expired.forEach((key) => this.cache.delete(key));
}
getStats(): CacheStats {
let totalHits = 0;
let totalSize = 0;
for (const entry of this.cache.values()) {
totalHits += entry.accessCount;
totalSize += JSON.stringify(entry.value).length;
}
return {
entries: this.cache.size,
maxSize: this.maxSize,
totalHits,
averageHits: totalHits / Math.max(this.cache.size, 1),
memoryUsage: totalSize,
};
}
}
interface CacheEntry {
value: any;
timestamp: number;
accessCount: number;
lastAccessed: number;
ttl: number;
}
interface CacheStats {
entries: number;
maxSize: number;
totalHits: number;
averageHits: number;
memoryUsage: number;
}
Distributed Caching: Scaling Beyond a Single Machine
Building a Distributed Cache Layer
// Distributed caching with consistent hashing
class DistributedCache {
private nodes: CacheNode[];
private hashRing: ConsistentHash;
private replicationFactor: number;
constructor(nodes: string[], replicationFactor: number = 2) {
this.replicationFactor = replicationFactor;
this.nodes = nodes.map((address) => new CacheNode(address));
this.hashRing = new ConsistentHash(this.nodes.map((node) => node.address));
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
const nodes = this.getNodes(key);
// Write to multiple nodes for redundancy
const writePromises = nodes
.slice(0, this.replicationFactor)
.map((node) => this.writeToNode(node, key, value, ttl));
// Wait for majority write (for consistency)
const majorityCount = Math.floor(this.replicationFactor / 2) + 1;
const results = await Promise.allSettled(writePromises);
const successCount = results.filter((r) => r.status === "fulfilled").length;
if (successCount < majorityCount) {
throw new Error("Failed to achieve majority write for cache entry");
}
}
async get(key: string): Promise<any> {
const nodes = this.getNodes(key);
// Try to read from nodes until we get a value
for (const node of nodes.slice(0, this.replicationFactor)) {
try {
const value = await this.readFromNode(node, key);
if (value !== null) {
return value;
}
} catch (error) {
console.warn(`Failed to read from node ${node.address}:`, error);
}
}
return null;
}
async delete(key: string): Promise<void> {
const nodes = this.getNodes(key);
// Delete from all replica nodes
const deletePromises = nodes
.slice(0, this.replicationFactor)
.map((node) => this.deleteFromNode(node, key));
await Promise.allSettled(deletePromises);
}
private getNodes(key: string): CacheNode[] {
const nodeAddresses = this.hashRing.getNodes(key, this.replicationFactor);
return nodeAddresses.map(
(address) => this.nodes.find((node) => node.address === address)!
);
}
private async writeToNode(
node: CacheNode,
key: string,
value: any,
ttl: number
): Promise<void> {
if (!node.isHealthy()) {
throw new Error(`Node ${node.address} is unhealthy`);
}
const serialized = JSON.stringify({
value,
timestamp: Date.now(),
ttl: ttl * 1000, // Convert to milliseconds
});
await node.client.set(key, serialized, ttl);
}
private async readFromNode(node: CacheNode, key: string): Promise<any> {
if (!node.isHealthy()) {
return null;
}
const data = await node.client.get(key);
if (!data) return null;
try {
const parsed = JSON.parse(data);
// Check if expired
if (Date.now() - parsed.timestamp > parsed.ttl) {
await this.deleteFromNode(node, key);
return null;
}
return parsed.value;
} catch (error) {
console.error(`Failed to parse cached data from ${node.address}:`, error);
return null;
}
}
// Health monitoring and failover
async startHealthMonitoring(): Promise<void> {
setInterval(async () => {
for (const node of this.nodes) {
try {
await node.client.ping();
node.markHealthy();
} catch (error) {
console.error(`Node ${node.address} failed health check:`, error);
node.markUnhealthy();
}
}
}, 10000); // Check every 10 seconds
}
}
class CacheNode {
public client: Redis;
private healthy: boolean = true;
private lastHealthCheck: Date = new Date();
constructor(public address: string) {
this.client = new Redis(address);
}
isHealthy(): boolean {
return this.healthy;
}
markHealthy(): void {
this.healthy = true;
this.lastHealthCheck = new Date();
}
markUnhealthy(): void {
this.healthy = false;
this.lastHealthCheck = new Date();
}
}
Cache Partitioning and Sharding
// Intelligent cache partitioning based on data patterns
class PartitionedCache {
private hotDataCache: Redis; // Frequently accessed data
private warmDataCache: Redis; // Occasionally accessed data
private coldDataCache: Redis; // Rarely accessed data
private accessTracker: Map<string, AccessMetrics> = new Map();
constructor() {
// Different Redis instances optimized for different access patterns
this.hotDataCache = new Redis({
host: "hot-cache.redis.cluster",
maxRetriesPerRequest: 3,
lazyConnect: true,
});
this.warmDataCache = new Redis({
host: "warm-cache.redis.cluster",
maxRetriesPerRequest: 2,
lazyConnect: true,
});
this.coldDataCache = new Redis({
host: "cold-cache.redis.cluster",
maxRetriesPerRequest: 1,
lazyConnect: true,
});
// Periodically rebalance cache tiers
setInterval(() => this.rebalanceTiers(), 300000); // Every 5 minutes
}
async get(key: string): Promise<any> {
this.trackAccess(key);
const tier = this.determineDataTier(key);
try {
switch (tier) {
case "hot":
return await this.getFromHotCache(key);
case "warm":
return await this.getFromWarmCache(key);
case "cold":
return await this.getFromColdCache(key);
default:
// Try all tiers if tier is unknown
return await this.getFromAnyTier(key);
}
} catch (error) {
console.error(`Cache get failed for key ${key}:`, error);
return null;
}
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
const tier = this.determineDataTier(key);
const cache = this.getCacheForTier(tier);
await cache.setex(
key,
ttl,
JSON.stringify({
value,
tier,
timestamp: Date.now(),
})
);
this.trackAccess(key);
}
private determineDataTier(key: string): "hot" | "warm" | "cold" {
const metrics = this.accessTracker.get(key);
if (!metrics) {
return "cold"; // New keys start cold
}
const accessRate =
metrics.totalAccesses /
((Date.now() - metrics.firstAccess) / (1000 * 60 * 60)); // per hour
if (accessRate > 10) {
return "hot";
} else if (accessRate > 1) {
return "warm";
} else {
return "cold";
}
}
private async rebalanceTiers(): Promise<void> {
const keysToRebalance: Array<{
key: string;
currentTier: string;
targetTier: string;
}> = [];
for (const [key, metrics] of this.accessTracker.entries()) {
const currentTier = metrics.currentTier;
const targetTier = this.determineDataTier(key);
if (currentTier !== targetTier) {
keysToRebalance.push({ key, currentTier, targetTier });
}
}
// Move keys between tiers
for (const { key, currentTier, targetTier } of keysToRebalance) {
await this.moveKeyBetweenTiers(key, currentTier, targetTier);
}
console.log(`Rebalanced ${keysToRebalance.length} cache keys`);
}
private async moveKeyBetweenTiers(
key: string,
from: string,
to: string
): Promise<void> {
const sourceCache = this.getCacheForTier(from);
const targetCache = this.getCacheForTier(to);
const data = await sourceCache.get(key);
if (data) {
const parsed = JSON.parse(data);
parsed.tier = to;
await targetCache.setex(
key,
await sourceCache.ttl(key),
JSON.stringify(parsed)
);
await sourceCache.del(key);
// Update metrics
const metrics = this.accessTracker.get(key);
if (metrics) {
metrics.currentTier = to;
}
}
}
private trackAccess(key: string): void {
const now = Date.now();
let metrics = this.accessTracker.get(key);
if (!metrics) {
metrics = {
totalAccesses: 0,
firstAccess: now,
lastAccess: now,
currentTier: "cold",
};
this.accessTracker.set(key, metrics);
}
metrics.totalAccesses++;
metrics.lastAccess = now;
}
}
interface AccessMetrics {
totalAccesses: number;
firstAccess: number;
lastAccess: number;
currentTier: string;
}
Cache Invalidation: The Two Hard Things in Computer Science
Smart Cache Invalidation Strategies
// Event-driven cache invalidation
class SmartCacheInvalidator {
private redis: Redis;
private dependencies: Map<string, Set<string>> = new Map();
private invalidationQueue: Queue;
constructor() {
this.redis = new Redis();
this.invalidationQueue = new Queue("cache-invalidation");
this.setupInvalidationProcessor();
}
// Register cache dependencies
registerDependency(cacheKey: string, dependsOn: string[]): void {
for (const dependency of dependsOn) {
if (!this.dependencies.has(dependency)) {
this.dependencies.set(dependency, new Set());
}
this.dependencies.get(dependency)!.add(cacheKey);
}
}
// Invalidate cache with dependency cascade
async invalidate(
key: string,
options: InvalidationOptions = {}
): Promise<void> {
const invalidationId = uuidv4();
const toInvalidate: Set<string> = new Set([key]);
// Find all dependent cache keys
if (options.cascade !== false) {
this.collectDependencies(key, toInvalidate);
}
// Queue invalidation job
await this.invalidationQueue.add("invalidate-cache", {
invalidationId,
keys: Array.from(toInvalidate),
reason: options.reason || "manual",
timestamp: Date.now(),
});
// If immediate invalidation requested
if (options.immediate) {
await this.performInvalidation(Array.from(toInvalidate));
}
}
private collectDependencies(key: string, toInvalidate: Set<string>): void {
const dependents = this.dependencies.get(key);
if (dependents) {
for (const dependent of dependents) {
if (!toInvalidate.has(dependent)) {
toInvalidate.add(dependent);
// Recursively collect dependencies
this.collectDependencies(dependent, toInvalidate);
}
}
}
}
private async performInvalidation(keys: string[]): Promise<void> {
const pipeline = this.redis.pipeline();
keys.forEach((key) => {
pipeline.del(key);
});
await pipeline.exec();
// Publish invalidation event for other cache layers
await this.redis.publish(
"cache:invalidated",
JSON.stringify({
keys,
timestamp: Date.now(),
})
);
}
// Time-based invalidation with smart prefetch
async scheduleInvalidation(
key: string,
invalidateAt: Date,
prefetchCallback?: () => Promise<any>
): Promise<void> {
const delay = invalidateAt.getTime() - Date.now();
if (delay > 0) {
await this.invalidationQueue.add(
"scheduled-invalidation",
{
key,
prefetchCallback: prefetchCallback
? prefetchCallback.toString()
: null,
},
{ delay }
);
}
}
// Probabilistic early expiration to prevent cache stampedes
async getWithProbabilisticExpiry(
key: string,
ttl: number,
refreshCallback: () => Promise<any>
): Promise<any> {
const data = await this.redis.get(key);
if (data) {
const parsed = JSON.parse(data);
const age = Date.now() - parsed.timestamp;
const timeLeft = ttl * 1000 - age;
// Probabilistically refresh before expiration
// Higher probability as we get closer to expiration
const refreshProbability = 1 - timeLeft / (ttl * 1000);
if (Math.random() < refreshProbability) {
// Refresh in background
this.refreshInBackground(key, ttl, refreshCallback);
}
return parsed.value;
}
// Cache miss - fetch and cache
return await this.fetchAndCache(key, ttl, refreshCallback);
}
private async refreshInBackground(
key: string,
ttl: number,
refreshCallback: () => Promise<any>
): Promise<void> {
try {
const freshValue = await refreshCallback();
await this.redis.setex(
key,
ttl,
JSON.stringify({
value: freshValue,
timestamp: Date.now(),
})
);
} catch (error) {
console.error(`Background refresh failed for key ${key}:`, error);
}
}
// Cache warming strategies
async warmCache(patterns: WarmingPattern[]): Promise<void> {
for (const pattern of patterns) {
await this.warmCachePattern(pattern);
}
}
private async warmCachePattern(pattern: WarmingPattern): Promise<void> {
const {
keyPrefix,
dataLoader,
batchSize = 100,
concurrency = 10,
} = pattern;
const keys = await this.getKeysToWarm(keyPrefix);
const batches = this.chunkArray(keys, batchSize);
// Process batches with controlled concurrency
await this.processWithConcurrency(
batches,
async (batch) => {
const data = await dataLoader(batch);
const pipeline = this.redis.pipeline();
for (const [key, value] of Object.entries(data)) {
pipeline.setex(
key,
pattern.ttl,
JSON.stringify({
value,
timestamp: Date.now(),
})
);
}
await pipeline.exec();
},
concurrency
);
}
}
interface InvalidationOptions {
cascade?: boolean;
immediate?: boolean;
reason?: string;
}
interface WarmingPattern {
keyPrefix: string;
dataLoader: (keys: string[]) => Promise<Record<string, any>>;
ttl: number;
batchSize?: number;
concurrency?: number;
}
Tag-Based Cache Invalidation
// Advanced tag-based invalidation system
class TaggedCacheService {
private redis: Redis;
private tagPrefix = "tag:";
async setWithTags(
key: string,
value: any,
ttl: number,
tags: string[]
): Promise<void> {
const pipeline = this.redis.pipeline();
// Store the actual cache entry
pipeline.setex(
key,
ttl,
JSON.stringify({
value,
tags,
timestamp: Date.now(),
})
);
// Associate cache key with each tag
tags.forEach((tag) => {
const tagKey = `${this.tagPrefix}${tag}`;
pipeline.sadd(tagKey, key);
pipeline.expire(tagKey, ttl + 300); // Keep tags slightly longer
});
await pipeline.exec();
}
async invalidateByTag(tag: string): Promise<void> {
const tagKey = `${this.tagPrefix}${tag}`;
// Get all cache keys associated with this tag
const cacheKeys = await this.redis.smembers(tagKey);
if (cacheKeys.length > 0) {
const pipeline = this.redis.pipeline();
// Delete all cache entries
cacheKeys.forEach((cacheKey) => {
pipeline.del(cacheKey);
});
// Remove the tag itself
pipeline.del(tagKey);
await pipeline.exec();
console.log(
`Invalidated ${cacheKeys.length} cache entries for tag: ${tag}`
);
}
}
async invalidateByMultipleTags(
tags: string[],
operator: "AND" | "OR" = "OR"
): Promise<void> {
const tagKeys = tags.map((tag) => `${this.tagPrefix}${tag}`);
let keysToInvalidate: string[];
if (operator === "OR") {
// Union: invalidate if associated with ANY of the tags
keysToInvalidate = await this.redis.sunion(...tagKeys);
} else {
// Intersection: invalidate only if associated with ALL tags
keysToInvalidate = await this.redis.sinter(...tagKeys);
}
if (keysToInvalidate.length > 0) {
await this.redis.del(...keysToInvalidate);
console.log(
`Invalidated ${
keysToInvalidate.length
} cache entries for tags: ${tags.join(", ")}`
);
}
}
// Example usage patterns
async cacheUserProfile(userId: string, profile: UserProfile): Promise<void> {
await this.setWithTags(
`profile:${userId}`,
profile,
3600, // 1 hour
[
`user:${userId}`,
`department:${profile.departmentId}`,
"user-profiles",
`role:${profile.role}`,
]
);
}
// When user is updated, invalidate all related caches
async onUserUpdated(userId: string): Promise<void> {
await this.invalidateByTag(`user:${userId}`);
}
// When department changes, invalidate all users in that department
async onDepartmentChanged(departmentId: string): Promise<void> {
await this.invalidateByTag(`department:${departmentId}`);
}
// When role permissions change, invalidate all users with that role
async onRolePermissionsChanged(role: string): Promise<void> {
await this.invalidateByTag(`role:${role}`);
}
}
Performance Profiling and Optimization
Application Performance Monitoring
// Comprehensive performance monitoring
class PerformanceProfiler {
private metrics: MetricsCollector;
private activeProfiles: Map<string, ProfileSession> = new Map();
// Method-level performance profiling
profile<T>(name: string, fn: () => Promise<T>): Promise<T> {
return this.profileAsync(name, fn);
}
private async profileAsync<T>(
name: string,
fn: () => Promise<T>
): Promise<T> {
const sessionId = uuidv4();
const session = {
name,
startTime: performance.now(),
startMemory: process.memoryUsage(),
cpuUsageStart: process.cpuUsage(),
};
this.activeProfiles.set(sessionId, session);
try {
const result = await fn();
await this.recordSuccess(sessionId);
return result;
} catch (error) {
await this.recordError(sessionId, error);
throw error;
}
}
private async recordSuccess(sessionId: string): Promise<void> {
const session = this.activeProfiles.get(sessionId);
if (!session) return;
const endTime = performance.now();
const endMemory = process.memoryUsage();
const cpuUsageEnd = process.cpuUsage();
const duration = endTime - session.startTime;
const memoryDelta = endMemory.heapUsed - session.startMemory.heapUsed;
const cpuDelta = {
user: cpuUsageEnd.user - session.cpuUsageStart.user,
system: cpuUsageEnd.system - session.cpuUsageStart.system,
};
// Record metrics
this.metrics.histogram(`performance.${session.name}.duration`, duration);
this.metrics.histogram(
`performance.${session.name}.memory_delta`,
memoryDelta
);
this.metrics.histogram(
`performance.${session.name}.cpu_user`,
cpuDelta.user / 1000
);
this.metrics.histogram(
`performance.${session.name}.cpu_system`,
cpuDelta.system / 1000
);
// Detect performance anomalies
if (duration > 1000) {
// Slow operation (>1s)
this.metrics.increment(`performance.${session.name}.slow_operations`);
}
if (memoryDelta > 10 * 1024 * 1024) {
// High memory usage (>10MB)
this.metrics.increment(
`performance.${session.name}.high_memory_operations`
);
}
this.activeProfiles.delete(sessionId);
}
// Database query profiling
async profileDatabaseQuery<T>(
query: string,
params: any[],
executor: () => Promise<T>
): Promise<T> {
const queryHash = this.hashQuery(query);
const startTime = performance.now();
try {
const result = await executor();
const duration = performance.now() - startTime;
this.metrics.histogram(`db.query.${queryHash}.duration`, duration);
this.metrics.increment(`db.query.${queryHash}.success`);
// Log slow queries
if (duration > 500) {
// Slow query threshold
console.warn("Slow database query detected:", {
query,
params,
duration: `${duration.toFixed(2)}ms`,
});
this.metrics.increment("db.slow_queries.total");
}
return result;
} catch (error) {
this.metrics.increment(`db.query.${queryHash}.error`);
throw error;
}
}
// Cache performance profiling
async profileCacheOperation<T>(
operation: "get" | "set" | "delete",
key: string,
executor: () => Promise<T>
): Promise<T> {
const startTime = performance.now();
try {
const result = await executor();
const duration = performance.now() - startTime;
this.metrics.histogram(`cache.${operation}.duration`, duration);
this.metrics.increment(`cache.${operation}.success`);
if (operation === "get") {
if (result !== null && result !== undefined) {
this.metrics.increment("cache.hits");
} else {
this.metrics.increment("cache.misses");
}
}
return result;
} catch (error) {
this.metrics.increment(`cache.${operation}.error`);
throw error;
}
}
// Memory leak detection
startMemoryLeakDetection(): void {
setInterval(() => {
const usage = process.memoryUsage();
this.metrics.gauge("memory.heap_used", usage.heapUsed);
this.metrics.gauge("memory.heap_total", usage.heapTotal);
this.metrics.gauge("memory.external", usage.external);
this.metrics.gauge("memory.rss", usage.rss);
// Alert on memory growth
const heapUsedMB = usage.heapUsed / (1024 * 1024);
if (heapUsedMB > 512) {
// Alert if heap > 512MB
console.warn(`High memory usage detected: ${heapUsedMB.toFixed(2)}MB`);
}
}, 30000); // Check every 30 seconds
}
// Generate performance report
async generatePerformanceReport(): Promise<PerformanceReport> {
const memoryUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
return {
timestamp: new Date(),
memory: {
heapUsed: memoryUsage.heapUsed / (1024 * 1024), // MB
heapTotal: memoryUsage.heapTotal / (1024 * 1024), // MB
external: memoryUsage.external / (1024 * 1024), // MB
rss: memoryUsage.rss / (1024 * 1024), // MB
},
cpu: {
user: cpuUsage.user / 1000000, // seconds
system: cpuUsage.system / 1000000, // seconds
},
cacheStats: await this.getCacheStats(),
databaseStats: await this.getDatabaseStats(),
activeOperations: this.activeProfiles.size,
};
}
private hashQuery(query: string): string {
// Simple hash for query identification (remove dynamic values)
const normalized = query
.replace(/\$\d+/g, "?") // Replace $1, $2, etc. with ?
.replace(/\s+/g, " ") // Normalize whitespace
.trim()
.toLowerCase();
return normalized.substring(0, 50); // Truncate for readability
}
}
interface ProfileSession {
name: string;
startTime: number;
startMemory: NodeJS.MemoryUsage;
cpuUsageStart: NodeJS.CpuUsage;
}
interface PerformanceReport {
timestamp: Date;
memory: {
heapUsed: number;
heapTotal: number;
external: number;
rss: number;
};
cpu: {
user: number;
system: number;
};
cacheStats: any;
databaseStats: any;
activeOperations: number;
}
Key Takeaways
Caching and performance optimization isn’t about adding Redis and calling it done. It’s about understanding data access patterns, designing for scale, and building intelligence into your cache layers.
Essential caching patterns:
- Multi-layer caching with memory → Redis → database hierarchy
- Smart invalidation with dependency tracking and tag-based clearing
- Distributed caching with consistent hashing and replication
- Performance profiling to identify bottlenecks before they become problems
The caching decision framework:
- Use in-memory caching for frequently accessed, small data
- Use Redis for shared cache across multiple instances
- Use Memcached for simple, high-performance string caching
- Use distributed caching when you need to scale beyond a single cache server
Cache invalidation strategies:
- Time-based expiration for data with predictable freshness requirements
- Event-driven invalidation for data that changes based on user actions
- Tag-based invalidation for complex dependency relationships
- Probabilistic early expiration to prevent cache stampedes
What’s Next?
In the next blog, we’ll dive into the operational side of performance optimization—database query optimization, connection pooling, memory management, and the scaling strategies that keep your applications running smoothly under any load.
We’ll also cover the monitoring and alerting systems that help you catch performance degradation before your users do, and the capacity planning techniques that prevent those 3 AM “everything is on fire” moments.
Because building fast systems is only half the battle—keeping them fast in production is where the real expertise shows.