ioredis is a free, open source databases project written in TypeScript and released under MIT. It has 15,355 GitHub stars, 1,272 forks and 202 open issues, and was last pushed 13 hours ago. On this registry it ranks #49 of 206 tracked projects in Databases, with 5 head-to-head comparisons available. It gained 11 stars over the last 30 tracked days.

What is ioredis?

ioredis is a robust, performance-focused and full-featured Redis client for Node.js, written entirely in TypeScript and released under the MIT licence, intended for Node.js developers and platform teams that need their applications to talk to Redis reliably.

What it is

ioredis lives in the Node.js ecosystem and targets the Redis server itself. It supports Redis >= 2.6.12 and is completely compatible with Redis 7.x. The library is 100% TypeScript and ships official type declarations, so consumers get typed APIs without a separate @types package. It is published under the MIT licence, carries version lines for 4.x, 5.x and 6.x, and is filed in this registry under Infrastructure & Operations / Databases.

The concrete problem it solves is that talking to Redis from an application is rarely just a socket and a command. A production Node.js service has to hold a connection open across topology changes, route commands to the right node, manage failover, batch work, run server-side scripts, subscribe to channels that may carry binary payloads, and secure the link. ioredis packages those concerns into a single client instead of leaving each team to assemble them by hand. Within the Node.js ecosystem it occupies the same slot as node-redis, the other Redis JavaScript client, and the README explicitly recommends node-redis for new projects โ€” so ioredis is best understood as the established, general-purpose client rather than the default first choice.

Key capabilities

  • Protocol and topology coverage for Redis Cluster, Sentinel, Streams and Pipelining, alongside Lua scripting, Redis Functions and Pub/Sub with support for binary messages.
  • Both Node.js callbacks and native promises are supported, so the same client fits either style of application code.
  • Transformation of command arguments and replies, plus transparent key prefixing.
  • An abstraction for Lua scripting that allows custom commands to be defined on the client.
  • TLS support, Redis ACL support, NAT mapping, an offline queue and ready checking.
  • Support for ES6 types such as Map and Set, GEO commands, and binary data.
  • Autopipelining and a sophisticated error handling strategy.

Who uses it and how

  • The README states that ioredis is used at Alibaba and many other companies, which indicates deployment at very large online commerce scale.
  • Teams running a sharded Redis deployment use it through the redis-cluster topic, letting the client handle cluster routing.
  • Teams that need high availability rather than sharding use it with Sentinel, covered by the redis-sentinel topic.
  • Projects that extend the server with redis-module workloads use it as the access layer from Node.js.
  • Version choice tracks the runtime: 6.x.x on the main branch requires Node.js >= 20 with Redis 6.2 to latest, 5.x on the v5 branch requires Node.js >= 12, and 4.x on the v4 branch requires Node.js >= 8.

Getting started

The library is consumed as the ioredis package inside a Node.js project, with 6.x.x as the latest line. API documentation is published for the Redis and Cluster classes at redis.github.io/ioredis, and version history lives in CHANGELOG.md, which also covers upgrade paths from v5 to v6 and v4 to v5.

How it compares

The only comparable tool named in the facts is node-redis, which the README describes as the open-source (MIT licence) Redis JavaScript client library redesigned from the ground up and actively maintained. Both are MIT-licensed and neither imposes a paid tier or a hosted dependency, so the difference is scope rather than licence: node-redis supports new and future commands, including hash-field expiration, and the capabilities available in Redis Stack and Redis 8 such as search, JSON, time-series and probabilistic data structures. The maintainers therefore point new projects at node-redis while continuing to support ioredis for existing users.

When to use it โ€” and when not to

A self-hoster still runs the Redis server, its Cluster or Sentinel topology if used, and the TLS certificates that the client's TLS options require โ€” ioredis is a client library and operates none of that. Teams starting a new project should follow the README's own advice and evaluate node-redis first, particularly if they need Redis Stack or Redis 8 features such as search, JSON, time-series or probabilistic data structures. The honest limitation is maintenance posture: the README states that ioredis is stable but maintained on a best-effort basis for relevant issues, and the repository carries 202 open issues, with recent activity dated 2026-09-14.

project readme (upstream, from github) โ€” read inline

ioredis

Build Status Coverage Status Commitizen friendly semantic-release

Discord Twitch YouTube Twitter

A robust, performance-focused and full-featured Redis client for Node.js.

Supports Redis >= 2.6.12. Completely compatible with Redis 7.x.

ioredis is a stable project and maintenance is done on a best-effort basis for relevant issues (contributions to ioredis will still be evaluated, reviewed, and merged when they benefit the project). For new projects, node-redis is the recommended client library. node-redis is the open-source (MIT license) Redis JavaScript client library redesigned from the ground up and actively maintained. node-redis supports new (hash-field expiration) and future commands and the capabilities available in Redis Stack and Redis 8 (search, JSON, time-series, probabilistic data structures).

Features

ioredis is a robust, full-featured Redis client that is used in the world's biggest online commerce company Alibaba and many other awesome companies.

  1. Full-featured. It supports Cluster, Sentinel, Streams, Pipelining, and of course Lua scripting, Redis Functions, Pub/Sub (with the support of binary messages).
  2. High performance ๐Ÿš€.
  3. Delightful API ๐Ÿ˜„. It works with Node callbacks and Native promises.
  4. Transformation of command arguments and replies.
  5. Transparent key prefixing.
  6. Abstraction for Lua scripting, allowing you to define custom commands.
  7. Supports binary data.
  8. Supports TLS ๐Ÿ”’.
  9. Supports offline queue and ready checking.
  10. Supports ES6 types, such as Map and Set.
  11. Supports GEO commands ๐Ÿ“.
  12. Supports Redis ACL.
  13. Sophisticated error handling strategy.
  14. Supports NAT mapping.
  15. Supports autopipelining.

100% written in TypeScript and official declarations are provided:

Versions

Version Branch Node.js Version Redis Version
6.x.x (latest) main >= 20 6.2 ~ latest
5.x.x v5 >= 12 2.6.12 ~ latest
4.x.x v4 >= 8 2.6.12 ~ 7

Refer to CHANGELOG.md for features and bug fixes introduced in v5.

๐Ÿš€ Upgrading from v5 to v6

๐Ÿš€ Upgrading from v4 to v5

Links


Quick Start

Install

npm install ioredis

In a TypeScript project, you may want to add TypeScript declarations for Node.js:

npm install --save-dev @types/node

Basic Usage

// Import ioredis.
// You can also use `import { Redis } from "ioredis"`
// if your project is a TypeScript project,
// Note that `import Redis from "ioredis"` is still supported,
// but will be deprecated in the next major version.
const Redis = require("ioredis");

// Create a Redis instance.
// By default, it will connect to localhost:6379.
// We are going to cover how to specify connection options soon.
const redis = new Redis();

redis.set("mykey", "value"); // Returns a promise which resolves to "OK" when the command succeeds.

// ioredis supports the node.js callback style
redis.get("mykey", (err, result) => {
  if (err) {
    console.error(err);
  } else {
    console.log(result); // Prints "value"
  }
});

// Or ioredis returns a promise if the last argument isn't a function
redis.get("mykey").then((result) => {
  console.log(result); // Prints "value"
});

redis.zadd("sortedSet", 1, "one", 2, "dos", 4, "quatro", 3, "three");
redis.zrange("sortedSet", 0, 2, "WITHSCORES").then((elements) => {
  // ["one", "1", "dos", "2", "three", "3"] as if the command was `redis> ZRANGE sortedSet 0 2 WITHSCORES`
  console.log(elements);
});

// All arguments are passed directly to the redis server,
// so technically ioredis supports all Redis commands.
// The format is: redis[SOME_REDIS_COMMAND_IN_LOWERCASE](ARGUMENTS_ARE_JOINED_INTO_COMMAND_STRING)
// so the following statement is equivalent to the CLI: `redis> SET mykey hello EX 10`
redis.set("mykey", "hello", "EX", 10);

See the examples/ folder for more examples. For example:

All Redis commands are supported. See the documentation for details.

Connect to Redis

When a new Redis instance is created, a connection to Redis will be created at the same time. You can specify which Redis to connect to by:

new Redis(); // Connect to 127.0.0.1:6379
new Redis(6380); // 127.0.0.1:6380
new Redis(6379, "192.168.1.1"); // 192.168.1.1:6379
new Redis("/tmp/redis.sock");
new Redis({
  port: 6379, // Redis port
  host: "127.0.0.1", // Redis host
  username: "default", // needs Redis >= 6
  password: "my-top-secret",
  db: 0, // Defaults to 0
});

You can also pass a connection string as the first constructor argument. Use a redis:// URL or rediss:// URL when using TLS encryption:

// Connect to 127.0.0.1:6380, db 4, using password "authpassword":
new Redis("redis://:authpassword@127.0.0.1:6380/4");

// Username can also be passed via URI.
new Redis("redis://username:authpassword@127.0.0.1:6380/4");

See API Documentation for all available options.

Pub/Sub

Redis provides several commands for developers to implement the Publishโ€“subscribe pattern. There are two roles in this pattern: publisher and subscriber. Publishers are not programmed to send their messages to specific subscribers. Rather, published messages are characterized into channels, without knowledge of what (if any) subscribers there may be.

By leveraging Node.js's built-in events module, ioredis makes pub/sub very straightforward to use. Below is a simple example that consists of two files, one is publisher.js that publishes messages to a channel, the other is subscriber.js that listens for messages on specific channels.

// publisher.js

const Redis = require("ioredis");
const redis = new Redis();

setInterval(() => {
  const message = { foo: Math.random() };
  // Publish to my-channel-1 or my-channel-2 randomly.
  const channel = `my-channel-${1 + Math.round(Math.random())}`;

  // Message can be either a string or a buffer
  redis.publish(channel, JSON.stringify(message));
  console.log("Published %s to %s", message, channel);
}, 1000);
// subscriber.js

const Redis = require("ioredis");
const redis = new Redis();

redis.subscribe("my-channel-1", "my-channel-2", (err, count) => {
  if (err) {
    // Just like other commands, subscribe() can fail for some reasons,
    // ex network issues.
    console.error("Failed to subscribe: %s", err.message);
  } else {
    // `count` represents the number of channels this client are currently subscribed to.
    console.log(
      `Subscribed successfully! This client is currently subscribed to ${count} channels.`
    );
  }
});

redis.on("message", (channel, message) => {
  console.log(`Received ${message} from ${channel}`);
});

// There's also an event called 'messageBuffer', which is the same as 'message' except
// it returns buffers instead of strings.
// It's useful when the messages are binary data.
redis.on("messageBuffer", (channel, message) => {
  // Both `channel` and `message` are buffers.
  console.log(channel, message);
});

It's worth noticing that a connection (aka a Redis instance) can't play both roles at the same time. More specifically, when a client issues subscribe() or psubscribe(), it enters the "subscriber" mode. From that point, only commands that modify the subscription set are valid. Namely, they are: subscribe, psubscribe, unsubscribe, punsubscribe, ping, and quit. When the subscription set is empty (via unsubscribe/punsubscribe), the connection is put back into the regular mode.

If you want to do pub/sub in the same file/process, you should create a separate connection:

const Redis = require("ioredis");
const sub = new Redis();
const pub = new Redis();

sub.subscribe(/* ... */); // From now, `sub` enters the subscriber mode.
sub.on("message" /* ... */);

setInterval(() => {
  // `pub` can be used to publish messages, or send other regular commands (e.g. `hgetall`)
  // because it's not in the subscriber mode.
  pub.publish(/* ... */);
}, 1000);

PSUBSCRIBE is also supported in a similar way when you want to subscribe all channels whose name matches a pattern:

redis.psubscribe("pat?ern", (err, count) => {});

// Event names are "pmessage"/"pmessageBuffer" instead of "message/messageBuffer".
redis.on("pmessage", (pattern, channel, message) => {});
redis.on("pmessageBuffer", (pattern, channel, message) => {});

Streams

Redis v5 introduces a new data type called streams. It doubles as a communication channel for building streaming architectures and as a log-like data structure for persisting data. With ioredis, the usage can be pretty straightforward. Say we have a producer publishes messages to a stream with redis.xadd("mystream", "*", "randomValue", Math.random()) (You may find the official documentation of Streams as a starter to understand the parameters used), to consume the messages, we'll have a consumer with the following code:

const Redis = require("ioredis");
const redis = new Redis();

const processMessage = (message) => {
  console.log("Id: %s. Data: %O", message[0], message[1]);
};

async function listenForMessage(lastId = "$") {
  // `results` is an array, each element of which corresponds to a key.
  // Because we only listen to one key (mystream) here, `results` only contains
  // a single element. See more: https://redis.io/commands/xread#return-value
  const results = await redis.xread("BLOCK", 0, "STREAMS", "mystream", lastId);
  const [key, messages] = results[0]; // `key` equals to "mystream"

  messages.forEach(processMessage);

  // Pass the last id of the results to the next round.
  await listenForMessage(messages[messages.length - 1][0]);
}

listenForMessage();

Expiration

Redis can set a timeout to expire your key, after the timeout has expired the key will be automatically deleted. (You can find the official Expire documentation to understand better the different parameters you can use), to set your key to expire in 60 seconds, we will have the following code:

redis.set("key", "data", "EX", 60);
// Equivalent to redis command "SET key data EX 60", because on ioredis set method,
// all arguments are passed directly to the redis server.

Handle Binary Data

Binary data support is out of the box. Pass buffers to send binary data:

redis.set("foo", Buffer.from([0x62, 0x75, 0x66]));

Every command that returns a bulk string has a variant command with a Buffer suffix. The variant command returns a buffer instead of a UTF-8 string:

const result = await redis.getBuffer("foo");
// result is `<Buffer 62 75 66>`

It's worth noticing that you don't need the Buffer suffix variant in order to send binary data. That means in most case you should just use redis.set() instead of redis.setBuffer() unless you want to get the old value with the GET parameter:

const result = await redis.setBuffer("foo", "new value", "GET");
// result is `<Buffer 62 75 66>` as `GET` indicates returning the old value.

Managed HIMPORT Fieldsets (experimental)

[!WARNING] ioredis managed-fieldset support is experimental and may change in a future release. When a managed HIMPORT SET needs fieldset preparation or recovery, later commands may be sent before that SET resumes. Await the SET before issuing commands that depend on its write.

HIMPORT requires Redis 8.10 or newer. It provides a faster ingestion mechanism for loading many hashes that share the same set of field names.

Explicit pipelines containing a configured HIMPORT SET wait for required fieldset preparation on their selected connection before the batch is sent. Unconfigured fieldsets remain manually managed.

const redis = new Redis({
  himportFieldsets: [
    {
      name: "user-profile",
      fields: ["name", "email", "age"],
    },
  ],
});

await redis.himport(
  "SET",
  "user:42",
  "user-profile",
  "Ada",
  "ada@example.com",
  "37"
);

For Cluster clients, pass himportFieldsets at the top level of the Cluster options, not under redisOptions. ioredis prepares managed fieldsets on current and future master connections:

const cluster = new Redis.Cluster(nodes, {
  himportFieldsets: [
    {
      name: "user-profile",
      fields: ["name", "email", "age"],
    },
  ],
});

Direct HIMPORT PREPARE, DISCARD, and DISCARDALL calls on a Cluster client fan out to all current masters, wait for every reply, and reject if any master fails. Inside an explicit pipeline, these commands remain connection-affine and are not managed.

Background preparation failures do not prevent a connection from becoming ready. Standalone and Sentinel clients report them through the error event; Cluster clients report them through node error. A dependent managed HIMPORT SET retries preparation and rejects if recovery fails.

See examples/himport.js for managed fieldsets and manual batches.

Pipelining

If you want to send a batch of commands (e.g. > 5), you can use pipelining to queue the commands in memory and then send them to Redis all at once. This way the performance improves by 50%~300% (See benchmark section).

redis.pipeline() creates a Pipeline instance. You can call any Redis commands on it just like the Redis instance. The commands are queued in memory and flushed to Redis by calling the exec method:

const pipeline = redis.pipeline();
pipeline.set("foo", "bar");
pipeline.del("cc");
pipeline.exec((err, results) => {
  // `err` is always null, and `results` is an array of responses
  // corresponding to the sequence of queued commands.
  // Each response follows the format `[err, result]`.
});

// You can even chain the commands:
redis
  .pipeline()
  .set("foo", "bar")
  .del("cc")
  .exec((err, results) => {});

// `exec` also returns a Promise:
const promise = redis.pipeline().set("foo", "bar").get("foo").exec();
promise.then((result) => {
  // result === [[null, 'OK'], [null, 'bar']]
});

Each chained command can also have a callback, which will be invoked when the command gets a reply:

redis
  .pipeline()
  .set("foo", "bar")
  .get("foo", (err, result) => {
    // result === 'bar'
  })
  .exec((err, result) => {
    // result[1][1] === 'bar'
  });

In addition to adding commands to the pipeline queue individually, you can also pass an array of commands and arguments to the constructor:

redis
  .pipeline([
    ["set", "foo", "bar"],
    ["get", "foo"],
  ])
  .exec(() => {
    /* ... */
  });

#length property shows how many commands in the pipeline:

const length = redis.pipeline().set("foo", "bar").get("foo").length;
// length === 2

Transaction

Most of the time, the transaction commands multi & exec are used together with pipeline. Therefore, when multi is called, a Pipeline instance is created automatically by default, so you can use multi just like pipeline:

redis
  .multi()
  .set("foo", "bar")
  .get("foo")
  .exec((err, results) => {
    // results === [[null, 'OK'], [null, 'bar']]
  });

If there's a syntax error in the transaction's command chain (e.g. wrong number of arguments, wrong command name, etc), then none of the commands would be executed, and an error is returned:

redis
  .multi()
  .set("foo")
  .set("foo", "new value")
  .exec((err, results) => {
    // err:
    //  { [ReplyError: EXECABORT Transaction discarded because of previous errors.]
    //    name: 'ReplyError',
    //    message: 'EXECABORT Transaction discarded because of previous errors.',
    //    command: { name: 'exec', args: [] },
    //    previousErrors:
    //     [ { [ReplyError: ERR wrong number of arguments for 'set' command]
    //         name: 'ReplyError',
    //         message: 'ERR wrong number of arguments for \'set\' command',
    //         command: [Object] } ] }
  });

In terms of the interface, multi differs from pipeline in that when specifying a callback to each chained command, the queueing state is passed to the callback instead of the result of the command:

redis
  .multi()
  .set("foo", "bar", (err, result) => {
    // result === 'QUEUED'
  })
  .exec(/* ... */);

If you want to use transaction without pipeline, pass { pipeline: false } to multi, and every command will be sent to Redis immediately without waiting for an exec invocation:

redis.multi({ pipeline: false });
redis.set("foo", "bar");
redis.get("foo");
redis.exec((err, result) => {
  // result === [[null, 'OK'], [null, 'bar']]
});

The constructor of multi also accepts a batch of commands:

redis
  .multi([
    ["set", "foo", "bar"],
    ["get", "foo"],
  ])
  .exec(() => {
    /* ... */
  });

Inline transactions are supported by pipeline, which means you can group a subset of commands in the pipeline into a transaction:

redis
  .pipeline()
  .get("foo")
  .multi()
  .set("foo", "bar")
  .get("foo")
  .exec()
  .get("foo")
  .exec();

Lua Scripting

ioredis supports all of the scripting commands such as EVAL, EVALSHA and SCRIPT. However, it's tedious to use in real world scenarios since developers have to take care of script caching and to detect when to use EVAL and when to use EVALSHA. ioredis exposes a defineCommand method to make scripting much easier to use:

const redis = new Redis();

// This will define a command myecho:
redis.defineCommand("myecho", {
  numberOfKeys: 2,
  lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
});

// Now `myecho` can be used just like any other ordinary command,
// and ioredis will try to use `EVALSHA` internally when possible for better performance.
redis.myecho("k1", "k2", "a1", "a2", (err, result) => {
  // result === ['k1', 'k2', 'a1', 'a2']
});

// `myechoBuffer` is also defined automatically to return buffers instead of strings:
redis.myechoBuffer("k1", "k2", "a1", "a2", (err, result) => {
  // result[0] equals to Buffer.from('k1');
});

// And of course it works with pipeline:
redis.pipeline().set("foo", "bar").myecho("k1", "k2", "a1", "a2").exec();

Dynamic Keys

If the number of keys can't be determined when defining a command, you can omit the numberOfKeys property and pass the number of keys as the first argument when you call the command:

redis.defineCommand("echoDynamicKeyNumber", {
  lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
});

// Now you have to pass the number of keys as the first argument every time
// you invoke the `echoDynamicKeyNumber` command:
redis.echoDynamicKeyNumber(2, "k1", "k2", "a1", "a2", (err, result) => {
  // result === ['k1', 'k2', 'a1', 'a2']
});

As Constructor Options

Besides defineCommand(), you can also define custom commands with the scripts constructor option:

const redis = new Redis({
  scripts: {
    myecho: {
      numberOfKeys: 2,
      lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
    },
  },
});

TypeScript Usages

You can refer to the example for how to declare your custom commands.

Transparent Key Prefixing

This feature allows you to specify a string that will automatically be prepended to all the keys in a command, which makes it easier to manage your key namespaces.

Warning This feature won't apply to commands like KEYS and SCAN that take patterns rather than actual keys(#239), and this feature also won't apply to the replies of commands even if they are key names (#325).

const fooRedis = new Redis({ keyPrefix: "foo:" });
fooRedis.set("bar", "baz"); // Actually sends SET foo:bar baz

fooRedis.defineCommand("myecho", {
  numberOfKeys: 2,
  lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
});

// Works well with pipelining/transaction
fooRedis
  .pipeline()
  // Sends SORT foo:list BY foo:weight_*->fieldname
  .sort("list", "BY", "weight_*->fieldname")
  // Supports custom commands
  // Sends EVALSHA xxx foo:k1 foo:k2 a1 a2
  .myecho("k1", "k2", "a1", "a2")
  .exec();

Transforming Arguments & Replies

Most Redis commands take one or more Strings as arguments, and replies are sent back as a single String or an Array of Strings. However, sometimes you may want something different. For instance, it would be more convenient if the HGETALL command returns a hash (e.g. { key: val1, key2: v2 }) rather than an array of key values (e.g. [key1, val1, key2, val2]).

ioredis has a flexible system for transforming arguments and replies. There are two types of transformers, argument transformer and reply transformer:

const Redis = require("ioredis");

// Here's the built-in argument transformer converting
// hmset('key', { k1: 'v1', k2: 'v2' })
// or
// hmset('key', new Map([['k1', 'v1'], ['k2', 'v2']]))
// into
// hmset('key', 'k1', 'v1', 'k2', 'v2')
Redis.Command.setArgumentTransformer("hmset", (args) => {
  if (args.length === 2) {
    if (args[1] instanceof Map) {
      // utils is a internal module of ioredis
      return [args[0], ...utils.convertMapToArray(args[1])];
    }
    if (typeof args[1] === "object" && args[1] !== null) {
      return [args[0], ...utils.convertObjectToArray(args[1])];
    }
  }
  return args;
});

// Here's the built-in reply transformer converting the HGETALL reply
// ['k1', 'v1', 'k2', 'v2']
// into
// { k1: 'v1', 'k2': 'v2' }
Redis.Command.setReplyTransformer("hgetall", (result) => {
  if (Array.isArray(result)) {
    const obj = {};
    for (let i = 0; i < result.length; i += 2) {
      obj[result[i]] = result[i + 1];
    }
    return obj;
  }
  return result;
});

There are three built-in transformers, two argument transformers for hmset & mset and a reply transformer for hgetall. Transformers for hmset and hgetall were mentioned above, and the transformer for mset is similar to the one for hmset:

redis.mset({ k1: "v1", k2: "v2" });
redis.get("k1", (err, result) => {
  // result === 'v1';
});

redis.mset(
  new Map([
    ["k3", "v3"],
    ["k4", "v4"],
  ])
);
redis.get("k3", (err, result) => {
  // result === 'v3';
});

Another useful example of a reply transformer is one that changes hgetall to return array of arrays instead of objects which avoids an unwanted conversation of hash keys to strings when dealing with binary hash keys:

Redis.Command.setReplyTransformer("hgetall", (result) => {
  const arr = [];
  for (let i = 0; i < result.length; i += 2) {
    arr.push([result[i], result[i + 1]]);
  }
  return arr;
});
redis.hset("h1", Buffer.from([0x01]), Buffer.from([0x02]));
redis.hset("h1", Buffer.from([0x03]), Buffer.from([0x04]));
redis.hgetallBuffer("h1", (err, result) => {
  // result === [ [ <Buffer 01>, <Buffer 02> ], [ <Buffer 03>, <Buffer 04> ] ];
});

RESP3 Protocol

ioredis uses RESP3 by default (protocol: 3), sending HELLO 3 on connect and transparently downgrading to RESP2 when the server can't do RESP3 (Redis Otherwise, ready will be emitted immediately right after the connect event. | | error | emits when an error occurs while connecting.
However, ioredis emits all error events silently (only emits when there's at least one listener) so that your application won't crash if you're not listening to the error event.
When redis.connect() is explicitly called the error will also be rejected from the returned promise, in addition to emitting it. If redis.connect() is not called explicitly and lazyConnect is true, ioredis will try to connect automatically on the first command and emit the error event silently. | | close | emits when an established Redis server connection has closed. | | reconnecting | emits after close when a reconnection will be made. The argument of the event is the time (in ms) before reconnecting. | | end | emits after close when no more reconnections will be made, or the connection is failed to establish. | | wait | emits when lazyConnect is set and will wait for the first command to be called before connecting. |

You can also check out the Redis#status property to get the current connection status.

Besides the above connection events, there are several other custom events:

Event Description
select emits when the database changed. The argument is the new db number.

Diagnostics Channel

ioredis publishes telemetry through Node.js diagnostics_channel, allowing APM tools and custom instrumentation to observe commands, connections, and batch operations without modifying application code.

These channels use TracingChannel#tracePromise() and emit start, end, asyncStart, asyncEnd, and error sub-events. Requires Node.js >= 18.19.0; on older versions the channels are silently unavailable (zero overhead). Subscribe via tracing:::

import dc from "node:diagnostics_channel";

dc.subscribe("tracing:ioredis:command:start", ({ command, args }) => {
  console.log(`> ${command}`, args);
});

dc.subscribe("tracing:ioredis:command:asyncEnd", ({ command }) => {
  console.log(`${command} settled`);
});

dc.subscribe("tracing:ioredis:command:error", ({ command, error }) => {
  console.error(`${command} failed:`, error);
});
Channel name Payload Description
ioredis:command CommandTraceContext Individual command (standalone, pipeline, or within MULTI)
ioredis:batch BatchOperationContext MULTI transaction as a whole
ioredis:connect ConnectTraceContext Socket connection attempt

Command arguments are sanitized before emission using rules adapted from @opentelemetry/redis-common. Sensitive values (e.g. values in SET, AUTH passwords) are replaced with ?, while read-only commands like GET and DEL retain all arguments. This is safe by default: unlisted or custom commands have all arguments redacted.

Context types (CommandTraceContext, BatchOperationContext, ConnectTraceContext) are exported from ioredis.

Offline Queue

When a command can't be processed by Redis (being sent before the ready event), by default, it's added to the offline queue and will be executed when it can be processed. You can disable this feature by setting the enableOfflineQueue option to false:

const redis = new Redis({ enableOfflineQueue: false });

TLS Options

Redis doesn't support TLS natively, however if the redis server you want to connect to is hosted behind a TLS proxy (e.g. stunnel) or is offered by a PaaS service that supports TLS connection (e.g. Redis.com), you can set the tls option:

const redis = new Redis({
  host: "localhost",
  tls: {
    // Refer to `tls.connect()` section in
    // https://nodejs.org/api/tls.html
    // for all supported options
    ca: fs.readFileSync("cert.pem"),
  },
});

Alternatively, pass a rediss:// URL directly to the constructor. The host field in RedisOptions should contain only the hostname; a rediss:// URL in host is not parsed as a TLS URL.

const redis = new Redis("rediss://redis.my-service.com");

If you do not want to use a connection string, you can also specify an empty tls: {} object:

const redis = new Redis({
  host: "redis.my-service.com",
  tls: {},
});

TLS Profiles

Warning TLS profiles described in this section are going to be deprecated in the next major version. Please provide TLS options explicitly.

To make it easier to configure we provide a few pre-configured TLS profiles that can be specified by setting the tls option to the profile's name or specifying a tls.profile option in case you need to customize some values of the profile.

Profiles:

  • RedisCloudFixed: Contains the CA for Redis.com Cloud fixed subscriptions
  • RedisCloudFlexible: Contains the CA for Redis.com Cloud flexible subscriptions
const redis = new Redis({
  host: "localhost",
  tls: "RedisCloudFixed",
});

const redisWithClientCertificate = new Redis({
  host: "localhost",
  tls: {
    profile: "RedisCloudFixed",
    key: "123",
  },
});

Sentinel

ioredis supports Sentinel out of the box. It works transparently as all features that work when you connect to a single node also work when you connect to a sentinel group. Make sure to run Redis >= 2.8.12 if you want to use this feature. Sentinels have a default port of 26379.

To connect using Sentinel, use:

const redis = new Redis({
  sentinels: [
    { host: "localhost", port: 26379 },
    { host: "localhost", port: 26380 },
  ],
  name: "mymaster",
});

redis.set("foo", "bar");

The arguments passed to the constructor are different from the ones you use to connect to a single node, where:

  • name identifies a group of Redis instances composed of a master and one or more slaves (mymaster in the example);
  • sentinelPassword (optional) password for Sentinel instances.
  • sentinels are a list of sentinels to connect to. The list does not need to enumerate all your sentinel instances, but a few so that if one is down the client will try the next one.
  • role (optional) with a value of slave will return a random slave from the Sentinel group.
  • preferredSlaves (optional) can be used to prefer a particular slave or set of slaves based on priority. It accepts a function or array.
  • enableTLSForSentinelMode (optional) set to true if connecting to sentinel instances that are encrypted

ioredis guarantees that the node you connected to is always a master even after a failover. When a failover happens, instead of trying to reconnect to the failed node (which will be demoted to slave when it's available again), ioredis will ask sentinels for the new master node and connect to it. All commands sent during the failover are queued and will be executed when the new connection is established so that none of the commands will be lost.

It's possible to connect to a slave instead of a master by specifying the option role with the value of slave and ioredis will try to connect to a random slave of the specified master, with the guarantee that the connected node is always a slave. If the current node is promoted to master due to a failover, ioredis will disconnect from it and ask the sentinels for another slave node to connect to.

If you specify the option preferredSlaves along with role: 'slave' ioredis will attempt to use this value when selecting the slave from the pool of available slaves. The value of preferredSlaves should either be a function that accepts an array of available slaves and returns a single result, or an array of slave values priorities by the lowest prio value first with a default value of 1.

// available slaves format
const availableSlaves = [{ ip: "127.0.0.1", port: "31231", flags: "slave" }];

// preferredSlaves array format
let preferredSlaves = [
  { ip: "127.0.0.1", port: "31231", prio: 1 },
  { ip: "127.0.0.1", port: "31232", prio: 2 },
];

// preferredSlaves function format
preferredSlaves = function (availableSlaves) {
  for (let i = 0; i < availableSlaves.length; i++) {
    const slave = availableSlaves[i];
    if (slave.ip === "127.0.0.1") {
      if (slave.port === "31234") {
        return slave;
      }
    }
  }
  // if no preferred slaves are available a random one is used
  return false;
};

const redis = new Redis({
  sentinels: [
    { host: "127.0.0.1", port: 26379 },
    { host: "127.0.0.1", port: 26380 },
  ],
  name: "mymaster",
  role: "slave",
  preferredSlaves: preferredSlaves,
});

Besides the retryStrategy option, there's also a sentinelRetryStrategy in Sentinel mode which will be invoked when all the sentinel nodes are unreachable during connecting. If sentinelRetryStrategy returns a valid delay time, ioredis will try to reconnect from scratch. The default value of sentinelRetryStrategy is:

function (times) {
  const delay = Math.min(times * 10, 1000);
  return delay;
}

Cluster

Redis Cluster provides a way to run a Redis installation where data is automatically sharded across multiple Redis nodes. You can connect to a Redis Cluster like this:

const Redis = require("ioredis");

const cluster = new Redis.Cluster([
  {
    port: 6380,
    host: "127.0.0.1",
  },
  {
    port: 6381,
    host: "127.0.0.1",
  },
]);

cluster.set("foo", "bar");
cluster.get("foo", (err, res) => {
  // res === 'bar'
});

Cluster constructor accepts two arguments, where:

  1. The first argument is a list of nodes of the cluster you want to connect to. Just like Sentinel, the list does not need to enumerate all your cluster nodes, but a few so that if one is unreachable the client will try the next one, and the client will discover other nodes automatically when at least one node is connected.

  2. The second argument is the options, where:

    • clusterRetryStrategy: When none of the startup nodes are reachable, clusterRetryStrategy will be invoked. When a number is returned, ioredis will try to reconnect to the startup nodes from scratch after the specified delay (in ms). Otherwise, an error of "None of startup nodes is available" will be returned. The default value of this option is:

      function (times) {
        const delay = Math.min(100 + times * 2, 2000);
        return delay;
      }
      

      It's possible to modify the startupNodes property in order to switch to another set of nodes here:

      function (times) {
        this.startupNodes = [{ port: 6790, host: '127.0.0.1' }];
        return Math.min(100 + times * 2, 2000);
      }
      
    • clusterNodeRetryStrategy: Per-node retry strategy. By default, ioredis won't try to reconnect to a lost node and just waits for a MOVED error to refresh slots. If you pass a function here, it's used as the retryStrategy for each node โ€” return a number to reconnect after that many ms, or anything else to drop the node from the pool. Handy for replica nodes that restart without slot changes (e.g. maintenance). Example:

      function (times) {
        return Math.min(100 * times, 2000);
      }
      
    • dnsLookup: Alternative DNS lookup function (dns.lookup() is used by default). It may be useful to override this in special cases, such as when AWS ElastiCache used with TLS enabled.

    • enableOfflineQueue: Similar to the enableOfflineQueue option of Redis class.

    • enableReadyCheck: When enabled, "ready" event will only be emitted when CLUSTER INFO command reporting the cluster is ready for handling commands. Otherwise, it will be emitted immediately after "connect" is emitted.

    • scaleReads: Config where to send the read queries. See below for more details.

    • subscriberNodeRole: Configure which node roles may be selected for the dedicated classic Pub/Sub subscriber connection. See below for more details.

    • maxRedirections: When a cluster related error (e.g. MOVED, ASK and CLUSTERDOWN etc.) is received, the client will redirect the command to another node. This option limits the max redirections allowed when sending a command. The default value is 16.

    • retryDelayOnFailover: If the target node is disconnected when sending a command, ioredis will retry after the specified delay. The default value is 100. You should make sure retryDelayOnFailover * maxRedirections > cluster-node-timeout to insure that no command will fail during a failover.

    • retryDelayOnClusterDown: When a cluster is down, all commands will be rejected with the error of CLUSTERDOWN. If this option is a number (by default, it is 100), the client will resend the commands after the specified time (in ms).

    • retryDelayOnTryAgain: If this option is a number (by default, it is 100), the client will resend the commands rejected with TRYAGAIN error after the specified time (in ms).

    • retryDelayOnMoved: By default, this value is 0 (in ms), which means when a MOVED error is received, the client will resend the command instantly to the node returned together with the MOVED error. However, sometimes it takes time for a cluster to become state stabilized after a failover, so adding a delay before resending can prevent a ping pong effect.

    • redisOptions: Default options passed to the constructor of Redis when connecting to a node.

    • slotsRefreshTimeout: Milliseconds before a timeout occurs while refreshing slots from the cluster (default 1000).

    • slotsRefreshInterval: Milliseconds between every automatic slots refresh (by default, it is disabled).

Read-Write Splitting

A typical redis cluster contains three or more masters and several slaves for each master. It's possible to scale out redis cluster by sending read queries to slaves and write queries to masters by setting the scaleReads option.

scaleReads is "master" by default, which means ioredis will never send any queries to slaves. There are other three available options:

  1. "all": Send write queries to masters and read queries to masters or slaves randomly.
  2. "slave": Send write queries to masters and read queries to slaves.
  3. a custom function(nodes, command): node: Will choose the custom function to select to which node to send read queries (write queries keep being sent to master). The first node in nodes is always the master serving the relevant slots. If the function returns an array of nodes, a random node of that list will be selected.

For example:

const cluster = new Redis.Cluster(
  [
    /* nodes */
  ],
  {
    scaleReads: "slave",
  }
);
cluster.set("foo", "bar"); // This query will be sent to one of the masters.
cluster.get("foo", (err, res) => {
  // This query will be sent to one of the slaves.
});

NB In the code snippet above, the res may not be equal to "bar" because of the lag of replication between the master and slaves.

Pub/Sub Subscriber Node Selection

Classic Pub/Sub in cluster mode uses one dedicated subscriber connection. By default, subscriberNodeRole is "all", so ioredis may select either a master or a slave for this connection. The available values are:

  1. "all": Select from all discovered cluster nodes.
  2. "master": Select only from master nodes.
  3. "slave": Select only from slave nodes.

For example, to select a master for the classic Pub/Sub subscriber connection:

const cluster = new Redis.Cluster(
  [
    /* nodes */
  ],
  {
    subscriberNodeRole: "master",
  }
);

cluster.subscribe("news");

This option is independent of scaleReads. Ordinary read-only commands such as GET and MGET are routed according to scaleReads, while subscriberNodeRole controls the dedicated connection used by SUBSCRIBE and PSUBSCRIBE. It does not affect PUBLISH or sharded Pub/Sub subscribers.

The configured role is applied whenever ioredis needs to select a subscriber. A role change alone does not replace an otherwise live subscriber connection. If no eligible node is available, the cluster remains ready and ordinary commands continue working, but subscription commands are rejected. Once a matching node is discovered, later subscription commands can proceed.

Running Commands to Multiple Nodes

Every command will be sent to exactly one node. For commands containing keys, (e.g. GET, SET and HGETALL), ioredis sends them to the node that serving the keys, and for other commands not containing keys, (e.g. INFO, KEYS and FLUSHDB), ioredis sends them to a random node.

Sometimes you may want to send a command to multiple nodes (masters or slaves) of the cluster, you can get the nodes via Cluster#nodes() method.

Cluster#nodes() accepts a parameter role, which can be "master", "slave" and "all" (default), and returns an array of Redis instance. For example:

// Send `FLUSHDB` command to all slaves:
const slaves = cluster.nodes("slave");
Promise.all(slaves.map((node) => node.flushdb()));

// Get keys of all the masters:
const masters = cluster.nodes("master");
Promise.all(
  masters
    .map((node) => node.keys())
    .then((keys) => {
      // keys: [['key1', 'key2'], ['key3', 'key4']]
    })
);

NAT Mapping

Sometimes the cluster is hosted within a internal network that can only be accessed via a NAT (Network Address Translation) instance. See Accessing ElastiCache from outside AWS as an example.

You can specify nat mapping rules via natMap option:

const cluster = new Redis.Cluster(
  [
    {
      host: "203.0.113.73",
      port: 30001,
    },
  ],
  {
    natMap: {
      "10.0.1.230:30001": { host: "203.0.113.73", port: 30001 },
      "10.0.1.231:30001": { host: "203.0.113.73", port: 30002 },
      "10.0.1.232:30001": { host: "203.0.113.73", port: 30003 },
    },
  }
);

Or you can specify this parameter through function:

const cluster = new Redis.Cluster(
  [
    {
      host: "203.0.113.73",
      port: 30001,
    },
  ],
  {
    natMap: (key) => {
      if (key.includes("30001")) {
        return { host: "203.0.113.73", port: 30001 };
      }

      return null;
    },
  }
);

This option is also useful when the cluster is running inside a Docker container. Also it works for Clusters in cloud infrastructure where cluster nodes connected through dedicated subnet.

Specifying through may be useful if you don't know concrete internal host and know only node port.

Transaction and Pipeline in Cluster Mode

Almost all features that are supported by Redis are also supported by Redis.Cluster, e.g. custom commands, transaction and pipeline. However there are some differences when using transaction and pipeline in Cluster mode:

  1. All keys in a pipeline should belong to slots served by the same node, since ioredis sends all commands in a

Frequently asked questions

Is ioredis free to use?

ioredis is open source under the MIT licence. There is no licence fee and no seat count โ€” you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does ioredis do?

๐Ÿš€ A robust, performance-focused, and full-featured Redis client for Node.js.

What is ioredis written in?

ioredis is primarily written in TypeScript. Its source is publicly available at https://github.com/redis/ioredis, and it has 15,355 GitHub stars.