Redis as a Performance Layer
Redis is an in-memory data store that provides sub-millisecond read/write latency. As a caching layer, Redis sits between your application and your database, dramatically reducing database load and response times. Beyond simple key-value caching, Redis supports data structures like sorted sets, lists, and streams that enable powerful patterns like rate limiting, real-time leaderboards, and pub/sub messaging.
When to Use Redis
- Session storage: Fast, shared sessions across server instances
- API response caching: Cache expensive query results for sub-ms reads
- Rate limiting: Sliding window counters for API rate limits
- Real-time features: Leaderboards (sorted sets), queues (lists), pub/sub
- Distributed locks: Prevent concurrent execution in distributed systems
Redis Client Setup
// lib/redis.ts — Redis client with connection pooling
import { Redis } from 'ioredis';
// Singleton pattern for connection reuse
let redis: Redis | null = null;
export function getRedis(): Redis {
if (!redis) {
redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: 0,
maxRetriesPerRequest: 3,
retryStrategy(times) {
const delay = Math.min(times * 50, 2000);
return delay;
},
// Connection pool settings
lazyConnect: true,
keepAlive: 30000,
});
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('connect', () => console.log('Redis connected'));
}
return redis;
}
// For Vercel KV (serverless-friendly Redis)
import { kv } from '@vercel/kv';
// Auto-managed connection, no pooling needed
await kv.set('key', 'value', { ex: 3600 });Caching Patterns with Redis
// Cache-aside pattern with serialization
const redis = getRedis();
async function getCachedData<T>(
key: string,
fetcher: () => Promise<T>,
ttlSeconds: number = 3600
): Promise<T> {
// 1. Try cache first
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached) as T;
}
// 2. Cache miss — fetch from source
const data = await fetcher();
// 3. Store in cache (non-blocking)
redis.set(key, JSON.stringify(data), 'EX', ttlSeconds).catch(console.error);
return data;
}
// Usage in API route
export async function GET(request: NextRequest) {
const products = await getCachedData(
'products:featured',
() => prisma.product.findMany({ where: { featured: true }, take: 10 }),
300 // 5 min TTL
);
return NextResponse.json(products);
}
// Cache with hash for structured data
async function cacheUserProfile(userId: string) {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) return null;
// Store as a hash — individual fields can be read/updated
await redis.hset(`user:${userId}`, {
name: user.name,
email: user.email,
plan: user.plan,
lastActive: new Date().toISOString(),
});
await redis.expire(`user:${userId}`, 3600);
return user;
}
// Read single field without deserializing entire object
async function getUserPlan(userId: string): Promise<string | null> {
return redis.hget(`user:${userId}`, 'plan');
}Rate Limiting with Redis
// Sliding window rate limiter
async function rateLimit(
identifier: string,
maxRequests: number,
windowSeconds: number
): Promise<{ allowed: boolean; remaining: number; resetIn: number }> {
const redis = getRedis();
const key = `ratelimit:${identifier}`;
const now = Date.now();
const windowMs = windowSeconds * 1000;
// Use a sorted set with timestamps as scores
const pipeline = redis.pipeline();
// Remove entries outside the window
pipeline.zremrangebyscore(key, 0, now - windowMs);
// Add current request
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// Count requests in window
pipeline.zcard(key);
// Set expiry on the key
pipeline.expire(key, windowSeconds);
const results = await pipeline.exec();
const requestCount = results?.[2]?.[1] as number || 0;
return {
allowed: requestCount <= maxRequests,
remaining: Math.max(0, maxRequests - requestCount),
resetIn: windowSeconds,
};
}
// Middleware for rate limiting
// middleware.ts
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const result = await rateLimit(ip, 100, 60); // 100 req/min
if (!result.allowed) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'Retry-After': String(result.resetIn),
'X-RateLimit-Remaining': String(result.remaining),
},
}
);
}
}
}Redis Performance Tips
- Use pipelines: Batch multiple commands into a single round trip
- Set TTLs on everything: Prevent memory leaks from forgotten keys
- Use appropriate data structures: Hashes for objects, sorted sets for rankings, lists for queues
- Monitor memory: Track Redis memory usage and eviction rates
- Use connection pooling: Reuse connections instead of creating new ones per request