ioredis
ioredis is the standard Redis client for Node.js: standalone, Sentinel, and Cluster support with TypeScript-friendly APIs.
Recipe
Quick-reference recipe card - copy-paste ready.
import Redis from "ioredis";
export const redis = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: false,
});
redis.on("error", (err) => {
console.error({ msg: "redis_error", err: err.message });
});
export async function cacheGet(key: string) {
return redis.get(key);
}When to reach for this:
- Shared cache, session store, rate limits, or BullMQ backend
- You need pipelines, Lua scripts, or Redis Cluster
- Managed Redis (ElastiCache, Upstash, Redis Cloud) with TLS URLs
Working Example
// src/redis.ts
import Redis from "ioredis";
function createRedis() {
const url = process.env.REDIS_URL!;
if (process.env.REDIS_CLUSTER === "true") {
return new Redis.Cluster([{ host: process.env.REDIS_HOST!, port: 6379 }], {
redisOptions: { password: process.env.REDIS_PASSWORD },
});
}
return new Redis(url, { maxRetriesPerRequest: 3 });
}
export const redis = createRedis();
// src/cache/batch.ts
export async function mgetJson<T>(keys: string[]): Promise<(T | null)[]> {
if (keys.length === 0) return [];
const pipeline = redis.pipeline();
keys.forEach((k) => pipeline.get(k));
const results = await pipeline.exec();
return (results ?? []).map(([err, val]) => {
if (err || val == null) return null;
return JSON.parse(val as string) as T;
});
}
// Graceful shutdown
process.on("SIGTERM", async () => {
await redis.quit();
});What this demonstrates:
- Factory for standalone vs Cluster
- Pipeline for multi-get without N round trips
quit()on shutdown for clean connection close
Deep Dive
Connection Options
| Option | Purpose |
|---|---|
maxRetriesPerRequest | Limit retries per command (BullMQ often sets null) |
connectTimeout | Fail fast when Redis unreachable |
tls | ElastiCache in-transit encryption |
lazyConnect | Defer connect until first command |
Pipelines vs Multi
const pipe = redis.pipeline();
pipe.set("a", "1");
pipe.incr("counter");
await pipe.exec();
await redis.mset("a", "1", "b", "2");- Pipeline: batch commands, one network round trip
MULTI/EXECtransactions: atomic when all keys same slot (Cluster caveat)
Lua Scripts
const script = `
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])
return 1
end
return 0
`;
await redis.eval(script, 1, "lock:job", "token", "30");- Atomic compare-and-set for locks and rate limits
- Define scripts once; use SHA in production for efficiency
Fastify Plugin
import fp from "fastify-plugin";
import { redis } from "./redis";
export const redisPlugin = fp(async (fastify) => {
fastify.decorate("redis", redis);
fastify.addHook("onClose", async () => {
await redis.quit();
});
});BullMQ Note
- BullMQ uses ioredis internally
- Workers often need
maxRetriesPerRequest: nullon blocking commands - separate client instances for BullMQ vs app cache
Gotchas
- New Redis per request - socket exhaustion. Fix: singleton export.
- Huge pipeline batches - memory spikes. Fix: chunk pipelines (500-1000 ops).
- Cluster cross-slot multi-key -
CROSSSLOTerrors. Fix: hash tags{tenant}:keyor separate keys. - No error handler - unhandled
errorevents crash Node. Fix:redis.on("error", ...). - Using Redis as primary database - persistence/AOF not a SQL replacement. Fix: cache + Postgres source of truth.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
node-redis (official) | Prefer official client | Need mature Cluster patterns today |
In-memory lru-cache | Single-instance dev tool | Multi-replica consistency |
| Memcached | Simple GET/SET at huge scale | Need data structures, streams, Lua |
| Upstash HTTP Redis | Edge without TCP Redis | Ultra-low latency LAN workloads |
FAQs
ioredis vs node-redis?
Both work on Node 24. ioredis is widely used with BullMQ and Cluster. Pick one per codebase.
How many Redis connections?
One per process for cache + dedicated BullMQ connections for workers. Sum across replicas.
TLS Redis URL?
Use rediss:// scheme or explicit tls: {} option per provider docs.
Connection pooling?
ioredis multiplexes on one connection for most commands. Blocking commands may need extra connections.
How do I test without Redis?
ioredis-mock for unit tests; Testcontainers Redis for integration tests.
Key expiration?
SET key val EX seconds or SETEX. Use TTL on every cache key by policy.
Pub/sub?
Separate subscriber connection - subscriber mode blocks other commands on same connection.
Redis 7 features?
Functions, ACL improvements - verify managed provider version before adopting.
NestJS integration?
Provide REDIS injection token wrapping singleton ioredis instance.
Observability?
Track command latency, error rate, connected clients metric from Redis INFO.
Related
- Caching Basics - cache-aside patterns
- Session Stores - express-session + Redis
- Distributed Locks - Redlock caveats
- BullMQ - queue backend
- Caching Best Practices - invalidation policy
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.