We earn commissions when you shop through the links below.
If you’ve ever watched your database buckle under read-heavy traffic, you already know the answer: cache aggressively. Knowing how to use Redis for caching in production is one of the highest-leverage skills a backend developer can have. Redis sits in front of your database, serves repeated reads from memory in microseconds, and gives your app the headroom to scale without rewriting a line of business logic.
This guide covers everything you need to go from a local Redis experiment to a hardened production setup — eviction policies, TTL strategy, connection pooling, cache invalidation, and deployment options.
Why Redis for Caching?
Redis is an in-memory data store that supports strings, hashes, lists, sets, sorted sets, and more. For caching purposes, the two things that matter most are:
- Speed — sub-millisecond reads from RAM vs. 10–100ms from a relational database.
- TTL support — every key can carry an expiry, so cache invalidation doesn’t require a separate cleanup job.
Unlike a simple in-process cache (e.g., a Node.js Map), Redis is shared across all application instances, which makes it safe in horizontally-scaled environments.
Installing and Connecting
For local development, Docker is the fastest path:
docker run -d --name redis -p 6379:6379 redis:7-alpineIn Node.js, use the ioredis library — it handles reconnection, cluster mode, and Sentinel out of the box:
npm install ioredis// lib/redis.js
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD,
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: false,
});
redis.on('error', (err) => console.error('Redis error:', err));
export default redis;Create the client once and reuse it across your application. Opening a new connection per request is a classic production footgun.
The Cache-Aside Pattern
Cache-aside (also called lazy loading) is the most common caching pattern and the right default for most use cases. The application checks Redis first; on a miss it fetches from the database and writes the result back to Redis.
// services/productService.js
import redis from '../lib/redis.js';
import db from '../lib/db.js';
const CACHE_TTL = 300; // seconds
export async function getProductById(id) {
const cacheKey = `product:${id}`;
// 1. Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. Cache miss — hit the database
const product = await db.query(
'SELECT * FROM products WHERE id = $1',
[id]
);
if (!product) return null;
// 3. Write to cache with TTL
await redis.set(cacheKey, JSON.stringify(product), 'EX', CACHE_TTL);
return product;
}
export async function invalidateProduct(id) {
await redis.del(`product:${id}`);
}Keep your cache keys namespaced (product:, user:, session:) so you can scan or delete groups of keys without guessing.
Choosing a TTL Strategy
There’s no universal TTL. Here’s how I think about it:
- Static reference data (countries, categories) — 24 hours or longer. This data rarely changes.
- User profile data — 5–15 minutes. Changes occasionally; stale for a few minutes is acceptable.
- Session tokens — match your session lifetime exactly.
- Real-time data (inventory counts, prices) — 30–60 seconds, or skip caching and use a read replica instead.
Add a small random jitter to your TTL to avoid thundering-herd cache stampedes when many keys expire simultaneously:
const jitter = Math.floor(Math.random() * 30); // 0–30 seconds
await redis.set(cacheKey, value, 'EX', CACHE_TTL + jitter);Eviction Policies for Production
In production Redis has a fixed maxmemory limit. When it’s full, Redis needs an eviction policy. Set this in redis.conf or at runtime:
# redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lruThe policies I reach for:
allkeys-lru— evict the least-recently-used key from the entire keyspace. Best general-purpose choice for a pure cache.volatile-lru— only evict keys that have a TTL set. Use this when Redis doubles as both cache and persistent store (e.g., sessions).allkeys-lfu— evict the least-frequently-used key. Better than LRU for skewed access patterns where a few keys are very hot.
Never leave maxmemory-policy at the default (noeviction) in a caching scenario — when memory fills up, writes will return errors and your app will crash.
Connection Pooling and Timeouts
In a high-traffic Node.js app, a single Redis connection can become a bottleneck. ioredis manages a single persistent connection internally, which is usually fine, but if you’re using blocking commands or pipelines heavily, consider a dedicated client per use case.
Always set a command timeout so a slow Redis doesn’t cascade into application slowness:
const redis = new Redis({
host: process.env.REDIS_HOST,
commandTimeout: 500, // ms — fail fast
connectTimeout: 2000,
});Wrap cache reads in try/catch and degrade gracefully to the database on Redis failure. Your cache is a performance optimization, not a hard dependency.
export async function getProductById(id) {
const cacheKey = `product:${id}`;
try {
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
} catch (err) {
console.warn('Redis read failed, falling back to DB:', err.message);
}
const product = await db.query(...);
try {
await redis.set(cacheKey, JSON.stringify(product), 'EX', CACHE_TTL);
} catch (err) {
console.warn('Redis write failed:', err.message);
}
return product;
}Deploying Redis in Production
Running Redis on the same server as your app is fine for low-traffic projects, but as you scale you’ll want a dedicated instance with persistence and replication. Two solid options:
DigitalOcean Managed Redis — a fully managed Redis cluster with automatic failover, daily backups, and VPC-level network isolation. You provision it in minutes, get a connection string, and never touch a config file. Ideal when you want Redis handled for you while you focus on application code.
Railway — if your app already runs on Railway, you can spin up a Redis instance from the same dashboard, connect it via private networking, and deploy together. The developer experience here is excellent for smaller teams who ship fast.
Whichever you choose, make sure your Redis instance is on the same private network as your application servers. A cross-region Redis call adds 30–100ms of latency and defeats much of the caching benefit.
Monitoring What Matters
Once you know how to use Redis for caching in production, the next question is: is it actually working? Track these metrics:
- Hit rate —
keyspace_hits / (keyspace_hits + keyspace_misses). Aim for above 80% on stable workloads. - Evicted keys — if this number climbs, your
maxmemoryis too low or your TTLs are too long. - Memory usage — watch
used_memory_rssvs yourmaxmemorylimit. - Connected clients — unexpected spikes indicate connection leaks.
You can pull all of this from INFO stats and INFO memory commands, or use the built-in Redis dashboard in managed providers.
# From redis-cli
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|evicted_keys'
redis-cli INFO memory | grep used_memory_humanCache Invalidation Done Right
Cache invalidation is famously hard. Here’s what works in practice:
- Time-based expiry (TTL) — the simplest approach. Accept that data may be stale for up to N seconds.
- Write-through invalidation — when you update a record, immediately delete (or rewrite) the cache key. Call
redis.del(cacheKey)inside the same service method that performs the database write. - Tag-based invalidation — store a set of related cache keys under a tag key, then delete the whole set when the underlying data changes. Useful for paginated list caches.
Avoid trying to update cache entries in-place across services. Delete the key and let the next request repopulate it — it’s simpler, less error-prone, and the performance hit of one extra DB query is negligible.
Final Thoughts
Understanding how to use Redis for caching in production comes down to a handful of decisions: pick cache-aside as your default pattern, set sensible TTLs with jitter, configure an LRU eviction policy, degrade gracefully on Redis failure, and deploy on a managed service that handles replication and failover for you.
Redis is remarkably simple to get started with, but production resilience is in the details — connection timeouts, eviction policies, and monitoring are what separate a toy cache from one you can trust at 3am. Get those right and Redis will be the most reliable piece of infrastructure you run.