Redis gets introduced to most stacks for one reason — caching — and then quietly ends up handling sessions, rate limiting, queues, and pub/sub, because an in-memory data structure server turns out to be useful for a lot more than just caching.
Redis is an in-memory key-value data store supporting rich data structures beyond simple strings — hashes, lists, sets, sorted sets, streams — with sub-millisecond read/write latency since data lives in RAM. It's most commonly used as a cache in front of a slower primary database, but its data structures make it a genuinely capable tool for sessions, rate limiting, leaderboards, and lightweight pub/sub messaging too.
Why Redis Matters (and When to Skip It)
A database query that takes 50ms becomes a Redis lookup that takes under 1ms — for read-heavy data that doesn't change every request (user sessions, computed aggregates, API responses), caching in Redis is often the single highest-leverage performance change available. Its data structures also solve problems that would be awkward in a relational database, like a sorted set for a real-time leaderboard with O(log n) rank lookups.
Skip Redis if your data needs durability guarantees stronger than what its persistence options provide, or if your read patterns are already fast enough directly against your primary database — adding a cache layer adds real complexity (cache invalidation, a new failure mode) that isn't worth it without an actual latency or load problem to solve.
Getting Started with Redis
Basic caching pattern — cache-aside (read through cache, fall back to source on miss):
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function getUser(id: string) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
await redis.set(`user:${id}`, JSON.stringify(user), { EX: 300 }); // 5 min TTL
return user;
}
Core Redis Concepts Every Developer Should Know
TTLs (time-to-live) are what make caching safe by default. Every cached key should expire, so stale data self-heals over time even if you miss an explicit invalidation somewhere. A cache with no TTLs is a slow-motion data-consistency bug waiting to happen.
Data structures beyond strings solve real problems elegantly. Sorted sets for leaderboards, hashes for structured objects without a separate serialization step, lists for simple queues:
// leaderboard via sorted set
await redis.zAdd("leaderboard", { score: 1500, value: "user_42" });
const top10 = await redis.zRange("leaderboard", 0, 9, { REV: true });
// rate limiting via INCR + EXPIRE
const count = await redis.incr(`rate:${userId}`);
if (count === 1) await redis.expire(`rate:${userId}`, 60);
if (count > 100) throw new Error("Rate limit exceeded");
Pub/Sub enables lightweight real-time messaging between processes, useful for broadcasting events across multiple server instances without standing up a dedicated message broker:
await redis.subscribe("notifications", (message) => {
console.log("Received:", message);
});
await redis.publish("notifications", JSON.stringify({ type: "new_post" }));
Persistence is optional and configurable — RDB snapshots and AOF (append-only file) logging let Redis survive restarts, but it's fundamentally an in-memory store first; treat it as a cache/ephemeral layer unless you've deliberately configured and tested it as a durable primary store.
Common Redis Mistakes and How to Fix Them
Mistake 1: caching without TTLs, relying entirely on manual invalidation. Manual invalidation is easy to miss in one code path, leaving stale data cached indefinitely. Fix: always set a TTL as a safety net, even alongside explicit invalidation logic.
Mistake 2: storing large values or unbounded collections in a single key. A massive JSON blob or an ever-growing list in one key creates a hotspot and slows down operations on that key. Fix: shard large datasets across multiple keys, or reconsider whether Redis is the right store for that specific data shape.
Mistake 3: using Redis as a primary data store without understanding its durability tradeoffs. Treating an in-memory store as your source of truth without configuring and testing persistence (AOF with appropriate fsync settings) risks real data loss on a crash. Fix: use Redis as a cache or ephemeral store by default; only use it as a primary store with deliberate persistence configuration and testing.
When Should You Use Redis Instead of In-Process Caching?
Use Redis when you need a shared cache across multiple server instances — an in-process cache (a plain JS Map, for example) isn't shared between horizontally scaled instances, so each one caches independently and inconsistently. Use in-process caching for single-instance apps or extremely hot, small data where even a Redis network round-trip is too slow.
Redis in Production
Use a managed Redis provider (Upstash, Redis Cloud, AWS ElastiCache) rather than self-hosting unless you have specific infrastructure reasons not to — replication, failover, and monitoring are meaningfully more work to manage yourself. Also monitor memory usage and eviction policy closely; Redis behavior under memory pressure depends entirely on your configured eviction policy (allkeys-lru, noeviction, etc.), and the default isn't always right for your use case.
If a specific query or computation is your current biggest latency contributor and it doesn't change every request, that's the first candidate worth caching in Redis — start there, not with caching everything by default.