cheat sheet
ioredis
Package-level reference for ioredis on npm — cluster, sentinel, pipelining, Lua scripts via defineCommand, and stability characteristics.
ioredis
What it is
ioredis is a robust, mature Redis client for Node — Promise-based, full feature coverage, and historically the strongest cluster/sentinel implementation. It originated at Alibaba and is now maintained under the Redis org. Its API surface is similar to the official redis client, but with different idioms (positional command arguments, built-in offline queue, eager-connect by default).
Reach for ioredis when you need production-grade cluster or sentinel support, when you prefer its argument-passing ergonomics, or when you're already on it and don't want to migrate to redis@5. Choose redis (the official package) for tighter TypeScript types and direct upstream alignment.
Install
npm install ioredis
Output: added ioredis to dependencies
pnpm add ioredis
Output: added 1 package, linked from store
yarn add ioredis
Output: added ioredis
bun add ioredis
Output: installed ioredis
Types ship in-tree — no @types/ioredis install needed.
Versioning & Node support
Current line is ioredis@5.x — stable for several years.
ioredis@5— Node 12.22+. Full TypeScript types in-tree. Tagged-templateclient.call("…")for arbitrary commands. Refined cluster behaviour.ioredis@4— Node 6+. Historical; the v4→v5 migration was largely additive.
ioredis@5 has been the current line since 2021 and is the recommended floor. Pin minor in production ("ioredis": "5.x").
Package metadata
- Maintainer: Zihua Li + Redis Ltd. (
redis/ioredis) - Project home: github.com/redis/ioredis
- Docs: github.com/redis/ioredis#readme
- npm: npmjs.com/package/ioredis
- License: MIT
- First released: 2015
- Downloads: ~5 million+ weekly downloads — historically more than
redisitself.
Peer dependencies & extras
ioredis has no peer-deps. Frequently-paired packages:
ioredis-mock— in-memory test stand-inredlock— distributed locks (works with bothioredisandnode-redis)bullmq— job queue; usesioredisinternallycache-manager-ioredis-yet— cache-manager backendconnect-redis— Express sessions (supports both clients)rate-limiter-flexible— Redis-backed rate limiting (supports both)
bullmq is the most notable downstream — it requires ioredis specifically, not redis.
Alternatives
| Client | Trade-off |
|---|---|
redis (node-redis) | Official upstream client. Cleaner types. Fewer historical features. |
tedis | Smaller alternative; niche. |
denoland/redis | For Deno. Different runtime. |
| Upstash Redis SDK | HTTP-based for serverless / edge. Different protocol. |
Real-world recipes
Single-node connection
ioredis connects eagerly by default — the first command queues until the connection is ready (no separate connect() call required).
import Redis from "ioredis";
const client = new Redis(process.env.REDIS_URL);
client.on("error", (err) => console.error("ioredis", err));
await client.set("greeting", "hello", "EX", 60);
const value = await client.get("greeting");
console.log(value);
await client.quit();
Output: hello. EX 60 expires the key in 60 seconds. ioredis accepts positional command args — SET key value EX 60 maps to client.set("key", "value", "EX", 60).
Cluster setup
Cluster is the cluster-aware variant. Pass a list of seed nodes; ioredis discovers the topology.
import { Cluster } from "ioredis";
const cluster = new Cluster(
[
{ host: "node1", port: 6379 },
{ host: "node2", port: 6379 },
{ host: "node3", port: 6379 },
],
{
redisOptions: { password: process.env.REDIS_PASSWORD },
scaleReads: "slave",
maxRedirections: 16,
retryDelayOnFailover: 100,
enableReadyCheck: true,
}
);
await cluster.set("user:1", "Alice");
console.log(await cluster.get("user:1"));
Output: Alice. scaleReads: "slave" (or all) routes reads to replicas; maxRedirections bounds MOVED/ASK chasing.
Multi-key commands across slots fail unless you use hash tags: {user}:1, {user}:2.
Sentinel setup
For Sentinel-based HA, pass sentinels: + the master name.
import Redis from "ioredis";
const client = new Redis({
sentinels: [
{ host: "sent1", port: 26379 },
{ host: "sent2", port: 26379 },
],
name: "mymaster",
password: process.env.REDIS_PASSWORD,
sentinelPassword: process.env.SENTINEL_PASSWORD,
role: "master", // or 'slave' for read-only replicas
});
Output: ioredis queries the sentinels for the current master and follows failovers automatically.
Pipeline (non-atomic batching)
pipeline() batches commands into one round-trip. Unlike multi(), it doesn't wrap them in a transaction.
import Redis from "ioredis";
const client = new Redis(process.env.REDIS_URL);
const pipeline = client.pipeline();
pipeline.set("user:1:name", "Alice");
pipeline.set("user:1:email", "alice@example.com");
pipeline.incr("user:count");
const results = await pipeline.exec();
console.log(results); // [[null, 'OK'], [null, 'OK'], [null, 5]]
await client.quit();
Output: [[null, 'OK'], [null, 'OK'], [null, 5]]. Each result is [error, response]. Errors are per-command, not per-pipeline.
For atomic batches, use client.multi() — same shape, with transaction semantics.
Lua script via defineCommand
ioredis can register a Lua script as a typed method via defineCommand. This is the cleanest way to deploy atomic multi-step operations.
import Redis from "ioredis";
const client = new Redis(process.env.REDIS_URL);
client.defineCommand("rateLimit", {
numberOfKeys: 1,
lua: `
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
if current > tonumber(ARGV[2]) then
return 0
end
return 1
`,
});
// TypeScript may need an explicit cast on the dynamic method
const allowed = await (client as any).rateLimit(`rl:user:1`, 60, 100);
console.log(allowed); // 1 (allowed) or 0 (blocked)
await client.quit();
Output: 1 if within rate, 0 if blocked. The script is cached at the server via EVALSHA — only the SHA is sent on repeat calls. Atomic: no race between INCR and EXPIRE.
Production deployment
Connection pool
ioredis maintains a single connection per client by default. For high throughput, the per-connection saturation point is around 100k commands/s — pipeline aggressively before scaling out.
For very high concurrency, run multiple Redis instances per process or fan out to multiple Node processes.
Cluster lifecycle
The Cluster client maintains one connection per node. Memory and FD usage grow with cluster size — for very large clusters (>100 nodes), audit your ulimit -n.
Reconnect strategy
ioredis reconnects automatically with exponential backoff. Customize:
const client = new Redis({
host: "...",
retryStrategy: (times) => {
if (times > 10) return null; // give up after 10 attempts
return Math.min(times * 50, 2000);
},
maxRetriesPerRequest: 3,
});
Output: retries each command up to 3 times; reconnects up to 10 times with capped backoff.
maxRetriesPerRequest: 0 disables per-command retry (commands fail fast when the connection is down). bullmq requires maxRetriesPerRequest: null to keep blocking commands open across reconnects.
Graceful shutdown
for (const sig of ["SIGTERM", "SIGINT"]) {
process.on(sig, async () => {
await client.quit();
process.exit(0);
});
}
Output: flushes pending commands; closes socket; clean exit.
Memory and eviction
Same as the official client: configure maxmemory + maxmemory-policy on the server. ioredis is unaware of server-side eviction; cached entries can vanish at any time.
Performance tuning
- Pipeline aggressively. RTT dominates Redis latency at any meaningful scale. Batched
pipeline()halves total time for any independent command sequence. enableOfflineQueue: falseto fail fast when disconnected — default queues commands until reconnect, which can mask outages.- Lua scripts for atomic multi-step ops. Avoid round-tripping conditional logic.
scaleReads: "slave"in clusters offloads reads to replicas. Read-after-write requiresmaster.maxRetriesPerRequestcarefully. Default 20 means slow failure on Redis outages. Lower to 2-3 for snappier error handling.commandTimeoutcaps per-command latency.commandTimeout: 1000rejects after 1s.- Avoid
KEYS— same warning asnode-redis. UseSCANinstead. enableAutoPipelining: trueauto-batches commands issued in the same event-loop tick. Significant speedup on busy paths.
Version migration guide
ioredis@5 has been the stable line for years. The notable transitions:
From ioredis@4 to ioredis@5
- Node 12.22+ required.
- TypeScript types moved in-tree; remove
@types/ioredisif present. - Several option defaults adjusted (
enableReadyCheckdefaults totrue). - Removed
client.dropBufferSupport— use theBuffer-suffixed method names instead. - Some cluster events renamed for consistency.
From redis@4/@5 to ioredis
If migrating because you need cluster/sentinel maturity:
createClient({ url })→new Redis(url).client.set("k", "v", { EX: 60 })→client.set("k", "v", "EX", 60)(positional args).client.multi().set(...).exec()returns[[err, val], ...]instead of[val, ...].- Pub/Sub:
client.subscribe(channel)+client.on("message", (channel, msg) => …)instead of passing a callback. - Connection is eager — no explicit
connect()call.
bullmq users are already on ioredis; no migration needed.
Stability characteristics
ioredis has been stable for years. Major releases are slow (5+ years on v5) and breaking changes are minimized. It is the right pick when you value:
- Battle-tested cluster + sentinel support
bullmqintegration (and other libraries that require ioredis specifically)- Positional command arguments matching
redis-clisyntax - Predictable behaviour through Node version bumps
Choose redis (node-redis) over ioredis when you want first-party upstream alignment, the latest RESP3 features, or tighter TypeScript types.
Security considerations
- TLS required for any network-traversing Redis. Pass
tls: {}ortls: { rejectUnauthorized: true, ca: ... }. - AUTH or ACLs. Production Redis must require auth. ioredis accepts
password:in options or in the URL. enableOfflineQueue: falseprevents silently queuing commands while the client is disconnected — important for write-correctness during outages.- Avoid
EVALwith user input. UsedefineCommand+ parameterized KEYS/ARGV, never string-concat user input into Lua source. MOVEDredirection in clusters can leak topology if logged. Audit log handlers.- Disable dangerous commands. Same as
node-redis: rename or ACL-restrictFLUSHALL,DEBUG,CONFIG. - Connection password in env, not source.
- Limit subscription channels. Unbounded pub/sub from untrusted clients amplifies attack surface.
Testing & CI integration
ioredis-mock is a drop-in in-memory stand-in for unit tests. It implements most commands faithfully.
import Redis from "ioredis-mock";
import { test, expect } from "vitest";
test("rate-limit script counts requests", async () => {
const client = new Redis();
await client.set("rl:user:1", "0");
await client.incr("rl:user:1");
expect(await client.get("rl:user:1")).toBe("1");
});
Output: sub-millisecond test; no Docker required.
For full-fidelity testing, run real Redis in a container:
services:
redis:
image: redis:7
ports: ["6379:6379"]
options: >-
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
ioredis-mock does not implement RedisJSON / RediSearch — modules require a real server.
Ecosystem integrations
| Tool | Role |
|---|---|
bullmq | Job queue. Requires ioredis specifically. |
bull (legacy) | Older job queue; predates BullMQ. |
connect-redis | Express session store. |
rate-limiter-flexible | Rate limiting; ioredis backend. |
cache-manager-ioredis-yet | cache-manager backend. |
redlock | Distributed locks. |
@socket.io/redis-adapter | Socket.IO multi-instance state. |
OpenTelemetry ioredis instrumentation | Per-command spans. |
agenda (with mongo backend, not redis) | Note: Agenda uses Mongo, not Redis. |
Troubleshooting common errors
MaxRetriesPerRequestError: Reached the max retries per request limit — server unreachable. Raise maxRetriesPerRequest or address the connectivity issue.
ClusterAllFailedError — cluster client couldn't reach any node. Check security groups / DNS / TLS.
MOVED <slot> <addr> — connected to a non-cluster client against a cluster. Switch to Cluster.
NOAUTH Authentication required — missing password. Set in URL or options.
READONLY You can't write against a read only replica — scaleReads: "slave" but a write was issued. Writes go to masters; reads can go to replicas.
Connection is closed — explicit quit() was called. Audit client lifecycle.
Stuck commands during failover — maxRetriesPerRequest too high. Lower to 2-3 for snappy failure.
Memory growth in subscriber — accumulating subscriptions. Use client.unsubscribe() when channels are done.
Lua script NOSCRIPT — script wasn't cached on the server (failover lost the cache). ioredis handles this automatically by re-sending the source.
bullmq stalled jobs — wrong ioredis config. BullMQ requires maxRetriesPerRequest: null and enableReadyCheck: false.
When NOT to use this
- Edge runtimes. ioredis uses TCP sockets — won't run on Workers / Vercel Edge. Use Upstash REST.
- You want first-party upstream alignment.
redis(node-redis) is the Redis-Ltd-maintained client and gets new features first. - You only need basic single-node Redis. Both clients work; pick the one your team already uses.
- You're not using
bullmqor similar ioredis-specific libraries. No strong reason to prefer ioredis when the officialredisworks. - TypeScript-strict projects.
redis@5has cleaner TypeScript types than ioredis.
See also
- npm: redis — official client (node-redis)
- Concept: async — event-driven patterns
- Concept: api — driver vs SDK boundaries