Redis caching patterns that work in production
Cache-aside, TTL discipline, and stampede prevention, with managed Redis on the private network and zero egress fees.
Priya Patel
Head of Engineering
Adding Redis does not automatically make your app fast. Bad cache keys, missing TTLs, and thundering herds can make things worse than hitting Postgres directly. This post covers the patterns we see work consistently on StackBlaze production workloads.
Provision Redis on the private network
services:
api:
type: web
build: .
env:
REDIS_URL:
from_service: cache
property: connection_string
cache:
type: redis
version: "7"
plan: standardTraffic between api and cache stays on the private fabric, sub-millisecond latency and no egress charges. Use the .internal hostname in logs if you debug connection issues.
Cache-aside (lazy loading)
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
export async function getUser(id: string) {
const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!user) return null;
await redis.set(key, JSON.stringify(user), 'EX', 300);
return user;
}Always set a TTL. Infinite keys turn Redis into a leakier second database with no schema. Five minutes is a reasonable default for user profile data; adjust based on how stale your UI can tolerate.
Preventing cache stampedes
When a hot key expires, every request may miss cache simultaneously and hammer your database. Mitigate with probabilistic early expiration, request coalescing, or a short-lived lock while one worker repopulates the key.
export async function getWithLock(key: string, loader: () => Promise<string>) {
const hit = await redis.get(key);
if (hit) return hit;
const lockKey = `lock:${key}`;
const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (!acquired) {
await sleep(50);
return getWithLock(key, loader);
}
try {
const value = await loader();
await redis.set(key, value, 'EX', 300);
return value;
} finally {
await redis.del(lockKey);
}
}Invalidation strategies
- Delete on write: simplest, when a user updates, DEL user:{id}
- Versioned keys: user:{id}:v{version} avoids races during updates
- Tag-based purge: store SET tags:org:{id} → [keys] for bulk invalidation
Do not cache what you cannot afford to lose
Sessions and rate-limit counters are fine in Redis. Financial balances and inventory counts should live in Postgres with Redis as an optional acceleration layer, not the system of record.
What to monitor
Watch hit rate (cache hits / total reads), memory usage, and evicted_keys. A dropping hit rate after a deploy often means a key format change. Rising evictions mean you need a larger plan or shorter TTLs.
Redis on StackBlaze includes persistence options on Standard plans and above. For pure cache workloads, disable AOF if you prefer speed over durability, you can always rebuild from Postgres.
Priya Patel
Head of Engineering at StackBlaze
Member of the founding team at StackBlaze. Writes about infrastructure, engineering culture, and the systems that keep production running.
More from the blog
How Calico network policies isolate tenants on shared hosting
Shared Kubernetes does not have to mean shared trust boundaries. Calico enforces network isolation, Linkerd provides automatic mTLS between services, and Falco detects runtime threats, three layers that keep tenants separated on shared infrastructure.
Shared platform vs dedicated clusters: control plane isolation and policy-as-code
Policy-as-code on a shared platform gives you guardrails without operational overhead. Dedicated clusters add an isolated control plane, single-tenant nodes, and customer-owned policy boundaries, here is how to choose and what changes under the hood.
Regulatory compliance and data governance on StackBlaze
SOC 2, GDPR, HIPAA-readiness, data residency, encryption, audit logs, and DPAs, a detailed map of how StackBlaze controls align with common regulatory frameworks and what you own vs what the platform certifies.