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

bash
npm install ioredis

Output: added ioredis to dependencies

bash
pnpm add ioredis

Output: added 1 package, linked from store

bash
yarn add ioredis

Output: added ioredis

bash
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-template client.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 redis itself.

Peer dependencies & extras

ioredis has no peer-deps. Frequently-paired packages:

  • ioredis-mock — in-memory test stand-in
  • redlock — distributed locks (works with both ioredis and node-redis)
  • bullmq — job queue; uses ioredis internally
  • cache-manager-ioredis-yet — cache-manager backend
  • connect-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

ClientTrade-off
redis (node-redis)Official upstream client. Cleaner types. Fewer historical features.
tedisSmaller alternative; niche.
denoland/redisFor Deno. Different runtime.
Upstash Redis SDKHTTP-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).

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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:

typescript
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

typescript
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: false to 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 requires master.
  • maxRetriesPerRequest carefully. Default 20 means slow failure on Redis outages. Lower to 2-3 for snappier error handling.
  • commandTimeout caps per-command latency. commandTimeout: 1000 rejects after 1s.
  • Avoid KEYS — same warning as node-redis. Use SCAN instead.
  • enableAutoPipelining: true auto-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

  1. Node 12.22+ required.
  2. TypeScript types moved in-tree; remove @types/ioredis if present.
  3. Several option defaults adjusted (enableReadyCheck defaults to true).
  4. Removed client.dropBufferSupport — use the Buffer-suffixed method names instead.
  5. Some cluster events renamed for consistency.

From redis@4/@5 to ioredis

If migrating because you need cluster/sentinel maturity:

  1. createClient({ url })new Redis(url).
  2. client.set("k", "v", { EX: 60 })client.set("k", "v", "EX", 60) (positional args).
  3. client.multi().set(...).exec() returns [[err, val], ...] instead of [val, ...].
  4. Pub/Sub: client.subscribe(channel) + client.on("message", (channel, msg) => …) instead of passing a callback.
  5. 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
  • bullmq integration (and other libraries that require ioredis specifically)
  • Positional command arguments matching redis-cli syntax
  • 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: {} or tls: { rejectUnauthorized: true, ca: ... }.
  • AUTH or ACLs. Production Redis must require auth. ioredis accepts password: in options or in the URL.
  • enableOfflineQueue: false prevents silently queuing commands while the client is disconnected — important for write-correctness during outages.
  • Avoid EVAL with user input. Use defineCommand + parameterized KEYS/ARGV, never string-concat user input into Lua source.
  • MOVED redirection in clusters can leak topology if logged. Audit log handlers.
  • Disable dangerous commands. Same as node-redis: rename or ACL-restrict FLUSHALL, 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.

typescript
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:

yaml
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

ToolRole
bullmqJob queue. Requires ioredis specifically.
bull (legacy)Older job queue; predates BullMQ.
connect-redisExpress session store.
rate-limiter-flexibleRate limiting; ioredis backend.
cache-manager-ioredis-yetcache-manager backend.
redlockDistributed locks.
@socket.io/redis-adapterSocket.IO multi-instance state.
OpenTelemetry ioredis instrumentationPer-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 replicascaleReads: "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 failovermaxRetriesPerRequest 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 bullmq or similar ioredis-specific libraries. No strong reason to prefer ioredis when the official redis works.
  • TypeScript-strict projects. redis@5 has cleaner TypeScript types than ioredis.

See also