ioredis is a full-featured Redis client for Node.js with first-class support for Cluster, Sentinel, pipelines, Pub/Sub, and Lua scripting. node-redis is the official Redis client, leaner, and now the recommended path for new projects. That is the short version. The longer version, the one that actually matters when you are shipping a production app, is that both libraries work fine on a single Redis instance and both libraries show their real character the moment you add a second node, a Sentinel, or a pipeline that has to survive a network blip.
Every list of “best Redis clients for Node.js” in 2026 is a coin flip between the two. The winner depends on the workload. This post is a working engineer’s take on which client to pick for which app, where each one shines, where each one bites, and how to keep the migration cost low if you change your mind later. It assumes you already know Redis exists, that you want a Node client, and that someone in the team is about to make a decision that will live in the codebase for two years.
Table of contents
- The short version
- What both clients do well
- Where ioredis still wins in 2026
- Where node-redis has caught up
- Cluster, Sentinel, and the part that decides it
- Pipelines, transactions, and Lua
- The Pub/Sub edge case
- How to pick without regretting it
- The connection-pooling trap that hits both
- What I would not do with either one
- FAQ
The short version
If you are starting a new Node project in 2026 and you only need a single Redis instance, node-redis is the recommended default. The Redis company maintains it, the API has been modernized to use native promises, the TypeScript types are first-class, and the package size is smaller. For 80% of small apps this is the right call.
If you are running Cluster or Sentinel, ioredis still has the smoother mental model. The Redis.Cluster constructor accepts an array of seed nodes and figures out the rest. node-redis supports Cluster, but the API requires more wiring and more places for the code to be honest about which node it is talking to.
If you are doing heavy pipelining, Lua scripting, or Pub/Sub fan-out, ioredis is the more battle-tested default. The pipeline API is a single chained call, the Lua integration is one line, and Pub/Sub reconnects are handled in the background.
The good news is that both clients are mature, both have clean TypeScript types, and the migration cost between them is usually measured in days, not weeks. Pick the one that matches the workload, not the one a blog post told you to pick.
What both clients do well
Before the comparison, it is worth saying what is not a difference. Both libraries do the basics correctly, and the basics are most of what most apps need.
Connection handling. Both clients handle TCP connections with a sensible default of one connection per client instance, automatic reconnection on network errors, and backoff that grows but does not break. Neither library silently drops a command during a reconnect, and both surface errors with a recognizable shape.
String and hash operations. GET, SET, HSET, HGETALL, EXPIRE, DEL. Both clients support the full surface of string and hash commands. The performance is within a few percent on a single-node Redis. If your only command is SET key value, you cannot measure a difference.
TypeScript types. Both clients ship TypeScript declarations. The shape is different, and which one you prefer is mostly a matter of taste. ioredis exposes a class you instantiate; node-redis exposes a createClient() factory. Both let you pass generic types for the values you read.
Pipelines. Both clients support pipelines that batch commands and ship them in one round trip. The shape of the API is different. ioredis chains .pipeline().set().get().exec(). node-redis uses client.multi() or the lower-level pipeline interface. Both are correct.
Promises and async/await. Both clients return promises. The original node-redis was callback-first and the modern version is promise-first. ioredis has been promise-first since the early versions. There is no functional difference in 2026.
Authentication and TLS. Both support AUTH, TLS, and the usual suspects. If you are connecting to a managed Redis like Upstash, ElastiCache, or a self-hosted cluster behind a private network, the configuration is roughly symmetric.
For most small apps, the differences above are not the reason to pick one over the other. The differences that matter start showing up at the edges.
Where ioredis still wins in 2026
ioredis has been the default choice for serious Node workloads for years. The reasons are specific, not vibes.
The cluster API is one constructor call. With ioredis, you start a Cluster client by passing an array of seed nodes. The library discovers the rest of the cluster topology, maintains a connection to every primary, and routes commands to the right node based on the key’s hash slot. The developer writes the same client.set('user:42', ...) and the library figures out which shard owns the key.
const Redis = require('ioredis');
const cluster = new Redis.Cluster([
{ host: 'redis-node-1', port: 6379 },
{ host: 'redis-node-2', port: 6379 },
{ host: 'redis-node-3', port: 6379 },
]);
await cluster.set('user:42', 'Sean');
const value = await cluster.get('user:42');
The mental model is “one logical Redis, many physical nodes.” You do not have to think about which shard owns which key, and the library surfaces a single error if a command is rejected.
Sentinel is one constructor call too. High-availability Redis deployments usually run Sentinel for automatic failover. ioredis accepts the Sentinel list directly and the client handles role discovery, master election, and reconnection when the master changes.
const sentinel = new Redis({
sentinels: [
{ host: 'sentinel-1', port: 26379 },
{ host: 'sentinel-2', port: 26379 },
{ host: 'sentinel-3', port: 26379 },
],
name: 'mymaster',
});
The failover is automatic. The new master is discovered through Sentinel, the client reconnects, and in-flight commands either succeed against the new master or fail with a recognizable error. For a production app, the boring “it just works” is the point.
Pipelines are chained and obvious. The ioredis pipeline reads like a script.
const results = await client.pipeline()
.set('user:1', 'Alice')
.set('user:2', 'Bob')
.incr('counter')
.expire('counter', 60)
.exec();
Each chained call returns a Pipeline you can keep chaining. The .exec() returns an array of [err, result] tuples. The shape is easy to read, easy to debug, and easy to teach.
Lua scripting is one call. The defineCommand method lets you wrap a Lua script as a method on the client.
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
return current
`,
});
const count = await client.rateLimit('user:42:requests', 60);
The script is sent once, cached by SHA, and reused on every call. The first call sends the full script; every call after that sends EVALSHA. This is the right pattern for atomic rate limiters, distributed locks, and any logic that needs to be atomic across multiple keys.
Pub/Sub reconnects are handled. Subscribing to a channel and then losing the connection is the most common Pub/Sub footgun. ioredis re-subscribes automatically when the connection comes back. node-redis does too, but the ioredis implementation is more obviously a single subscription call.
Where node-redis has caught up
For years, the official client was the slower-moving option. That is no longer the case. The 2024-2025 rewrites put node-redis on the same footing for most workloads, and the official blessing matters.
The Redis company maintains it. node-redis is the official client. New Redis features (Redis 7’s function library, search and JSON modules, the new vector commands) land in node-redis first or at the same time as ioredis. If you are betting on Redis sticking around and you want the new features, node-redis is the safer bet.
The API is now promise-first. The historical pain of node-redis was callbacks. That is fixed. The modern client returns promises, supports async/await, and the TypeScript story is good. There is no reason to avoid node-redis because of the API any more.
The TypeScript types are cleaner. Whether you prefer the factory function (createClient()) over the class instantiation, that is a style call. The types themselves are tighter and the generic value types are easier to use in practice.
Package size is smaller. node-redis has a smaller dependency footprint than ioredis. For serverless functions and edge runtimes where the cold start matters, this is a real difference. ioredis is around 200KB unpacked; node-redis is around 80KB.
The maintainer stance is now explicit. The Redis company recommends node-redis for new projects. This is not a knock on ioredis, which is still actively maintained and still the right choice for some workloads. It is a signal that the official client is the recommended default and the gap is smaller than it used to be.
Cluster, Sentinel, and the part that decides it
Most production decisions come down to one question: are you running a single Redis instance or a Cluster/Sentinel setup? The answer to that question picks the client for you.
Single instance, small to medium scale. node-redis is the right default. The API is cleaner, the types are tighter, the package is smaller, and you are not paying for cluster logic you do not need. If you are running one Redis and the worst case is “restart it,” node-redis is enough.
Cluster or Sentinel, production scale. ioredis is still the smoother choice. The constructor is a single call, the failover is automatic, and the developer mental model is “one Redis, many nodes.” node-redis supports both, but the API requires more explicit work and the failure modes are less forgiving.
Hybrid: Redis today, Cluster later. This is the case where the migration cost matters. Both libraries are easy to swap. The biggest friction is in the pipeline and Lua layers. ioredis’s chained pipeline is more idiomatic; node-redis’s multi() is more conventional. The decision to migrate later is real but not painful.
A useful test: if your app is going to live in production for more than six months, and your Redis is going to grow past a single node, start with ioredis. The setup cost is the same, and the future you will thank present you.
For managed Redis like Upstash (single instance, edge-friendly), AWS ElastiCache (Cluster, VPC-only), or self-hosted behind a private network, the client choice follows the same logic. Single node → node-redis. Cluster → ioredis.
Pipelines, transactions, and Lua
The three patterns most apps need beyond basic get/set are pipelines, transactions, and Lua. Both clients support all three. The shapes are different.
Pipelines batch commands and ship them in one round trip. The latency win is real. A pipeline of 100 commands takes roughly the time of one round trip plus the server-side execution. Without a pipeline, the same 100 commands are 100 round trips. For any high-throughput workload, pipelines are mandatory.
ioredis’s pipeline is a fluent chain. client.pipeline().set().get().exec(). Each method returns the pipeline, and .exec() returns the array of results.
node-redis’s pipeline is the same idea but with a different shape. The multi() method is the transaction wrapper; the lower-level pipeline is the same as ioredis’s. Either way, the underlying network behavior is the same.
Transactions (MULTI/EXEC) are different from pipelines. A transaction runs a sequence of commands atomically on the server. Pipelines do not. If you need atomicity, you need MULTI/EXEC. Both clients expose this.
Lua scripts are the third layer. Atomic, server-side, and useful for rate limiters, distributed locks, and any logic that has to touch multiple keys. ioredis’s defineCommand is the cleanest API I have used for this. node-redis supports EVAL directly, and the modern client has a defineScript helper. Both work.
The honest summary: if pipelines and Lua are the load-bearing part of your Redis usage, ioredis is the more idiomatic default. If transactions are the load-bearing part, either client works.
The Pub/Sub edge case
Pub/Sub is the part of Redis that catches people out. The protocol is fire-and-forget: a message published to a channel is delivered to every subscriber that is connected at that instant, and to no one if no one is connected. There is no persistence.
ioredis and node-redis both support Pub/Sub. The interesting difference is what happens when the connection drops.
ioredis auto-reconnects and re-subscribes. Your subscriber function keeps running; the library handles the resubscribe behind the scenes. If you are running a chat backend or a real-time event stream, this is the boring behavior you want.
node-redis also reconnects, but the resubscription is more explicit. You have to listen for the connect event and re-issue the subscribe. This is more code, but it is also more honest. If your subscriber has side effects on subscribe, you can see them.
The edge case that bites both libraries is: a message published during a reconnect is lost. Redis Pub/Sub is not durable. If durability matters, you want Redis Streams, not Pub/Sub. Both clients support Streams with roughly symmetric APIs.
How to pick without regretting it
The decision is simpler than the comparison posts make it look. Three questions.
Question 1: Are you running a single Redis or a Cluster/Sentinel?
- Single → node-redis.
- Cluster or Sentinel → ioredis.
Question 2: Are pipelines and Lua central to the design?
- Yes → ioredis (more idiomatic API for both).
- No → either, lean node-redis.
Question 3: Is the package size or cold start material?
- Yes (serverless, edge) → node-redis (smaller).
- No → either, lean ioredis for the cluster story.
If the answers are mixed, default to node-redis for new projects. The recommendation from the Redis company is the default for a reason. Migrate to ioredis if the cluster story becomes important, or if Lua and pipelines turn out to be the load-bearing part of the architecture.
For the deployment side, both clients work the same way on a platform like RunxBuild: the Node service connects to a managed Redis through an internal private network, the connection string lives in the environment variables, and the health check confirms the connection at boot. The client choice is a code-level decision, not a deploy-level decision.
The connection-pooling trap that hits both
Both clients recommend one connection per process. The “pool” in some libraries is a misnomer. ioredis is one connection by default; node-redis is one connection by default. If you see “connection pool” advertised for a Node Redis client, check the source.
The pattern that bites teams at scale is: process A is one Node instance, one connection. Process B is the same. Process C is the same. When a team moves from one Node process to ten Node processes behind a load balancer, they have ten connections. When they move to fifty processes, they have fifty. Redis is happy with this, but the egress cost on managed Redis services can become a line item.
The fix is not a connection pool. The fix is to:
- Reuse the client across requests in each process (do not create a new client per request).
- Use a serverless-friendly managed Redis if the workload is bursty.
- Connect over a private network so the connections do not count against public egress.
If you see a blog post recommending a “Redis connection pool for Node,” read the source. It is almost always a wrapper that creates clients on demand, which is the opposite of what you want.
What I would not do with either one
A short, opinionated list.
Do not store user data without a TTL. Both clients are happy to let you set keys without expiry. The memory grows, the cost grows, the bill at the end of the month is a surprise. Use EXPIRE on every key. The library can do this for you; the right pattern is client.set(key, value, 'EX', 3600).
Do not rely on Pub/Sub for anything that must not be lost. Pub/Sub is fire-and-forget. For durable messaging, use Redis Streams, Kafka, or a real queue. The library is not the issue; the protocol is.
Do not run a single Redis for production state. A single Redis is fine for caching, sessions that can be re-derived, and ephemeral data. For state that has to survive a node failure, run a Cluster or a managed Redis with replication. Both clients handle failover; neither library makes a single-node Redis durable.
Do not skip the health check. The simplest way to know your Node service can reach Redis is to run a PING in the health check. A service that boots but cannot reach its database is a service that will pass the readiness check and fail the first real request.
Do not log Redis commands at INFO in production. The library will happily log every command. That is fine in development. In production, log errors and slow commands, not every GET. The cost of logging is not the disk, it is the egress from your logging service.
FAQ
What is ioredis?
ioredis is a Node.js client for Redis that supports Cluster, Sentinel, pipelines, Pub/Sub, and Lua scripting. It is one of the two most-used Node Redis clients, alongside the official node-redis package. The project is actively maintained, the API is stable, and the library is the default choice for serious Node workloads that need Cluster or Sentinel.
Should I use ioredis or node-redis in 2026?
For new projects on a single Redis, the Redis company now recommends node-redis. For Cluster or Sentinel, ioredis is the smoother choice. For pipelines, Lua, and Pub/Sub, both work and ioredis is more idiomatic. The decision is usually driven by the deployment shape (single node vs. Cluster) more than by the API style.
Does ioredis support TypeScript?
Yes. ioredis ships TypeScript declarations and the types are accurate. The class-based API (new Redis(...)) makes generic value types straightforward. node-redis also has first-class TypeScript types, and the modern factory-style API is more idiomatic for some teams.
How do I run a Cluster with ioredis?
Create a Redis.Cluster instance with an array of seed nodes. The library discovers the full topology automatically. Pass at least two seed nodes so the client can recover if one is down. For Sentinel, pass a sentinels array and a name field. Both setups require a real Cluster or Sentinel deployment on the Redis side.
Is ioredis still maintained?
Yes. The project is actively maintained, the GitHub repo is responsive, and the library is on a regular release cadence. The Redis company’s recommendation of node-redis is not a deprecation of ioredis. It is a signal that the gap is smaller than it used to be.
Can I switch from ioredis to node-redis later?
Yes. Both libraries expose the same command surface for the most common operations. The migration cost is mostly in pipeline and Lua usage, which have different shapes. A typical migration is a few days for a small app, a few weeks for a large one. Plan the Lua rewrites carefully.
How do I check which version of ioredis I am running?
Run npm list ioredis in the project root. The library is also exported as a property on the Redis class (require('ioredis').Redis.version or require('ioredis/package.json').version in older versions). For production, pin the version in package.json and use npm ci to install it.
Does ioredis work with Upstash or ElastiCache?
Yes. ioredis works with any Redis-compatible service. Upstash is single-node and ioredis connects directly. ElastiCache can be single-node or Cluster, and ioredis handles both. The TLS and authentication configuration is the same as for a self-hosted Redis. The only thing to watch is the egress cost on a managed service — connecting every Node process directly can add up.