cheat sheet

redis

Package-level reference for node-redis on npm — pipelining, pub/sub, TTL, the v3-to-v4 API rewrite, and rate-limit patterns.

redis

What it is

redis (the official node-redis client, scoped under the redis org) is the canonical Redis client for Node. It is a Promise-first, TypeScript-typed driver covering the full Redis command surface — strings, hashes, sorted sets, streams, JSON, search, and pub/sub. The redis@4 release was a complete rewrite of the older callback-based API; redis@5 further tightens types and improves cluster support.

Reach for redis when you want the upstream-maintained client with strong types and a Promise-first API. Reach for ioredis when you want a more battle-tested cluster/sentinel implementation or you prefer its tagged-template fluency.

Install

bash
npm install redis

Output: added redis to dependencies

bash
pnpm add redis

Output: added 1 package, linked from store

bash
yarn add redis

Output: added redis

bash
bun add redis

Output: installed redis

Types ship in-tree. No @types/redis needed (the historical one targets the v3 API and should not be installed).

Versioning & Node support

Current line is redis@5.x.

  • redis@5 — Node 18+. Refined RESP3 handling, improved cluster client, full Promise+typed surface. Subscriber returns a typed callback signature.
  • redis@4 — Node 14+. The first version of the rewrite. Still widely used; migration to v5 is mostly mechanical.
  • redis@3 — Legacy callback API. Deprecated.

Redis Stack modules (RedisJSON, RediSearch, RedisTimeSeries) are accessible via command modules — import { createClient } from "redis"; client.json.get(...). The protocol is bundled.

Package metadata

  • Maintainer: Redis Ltd. (official, redis/node-redis)
  • Project home: github.com/redis/node-redis
  • Docs: redis.io/docs/latest/develop/clients/nodejs
  • npm: npmjs.com/package/redis
  • License: MIT
  • First released: 2010 (current API since 2021)
  • Downloads: ~3 million+ weekly downloads.

Peer dependencies & extras

No peer-deps. Companion packages:

  • @redis/client — core client (transitive)
  • @redis/json — RedisJSON command support
  • @redis/search — RediSearch / vector search
  • @redis/time-series — RedisTimeSeries
  • @redis/bloom — Bloom filters, Cuckoo, Count-Min, Top-K
  • @redis/graph — RedisGraph (note: RedisGraph is end-of-life upstream as of 2024)
  • connect-redis — Express session store
  • rate-limiter-flexible — rate-limit primitives with Redis backend
  • bullmq — job queue on Redis
  • redlock — distributed lock implementation

Alternatives

ClientTrade-off
ioredisOlder, very mature, robust cluster + sentinel support. Different API surface.
tedis / denoland/redisSmaller / Deno-oriented clients. Niche.
keyv (with redis backend)Cache abstraction that swaps backends. Useful when you might leave Redis later.
cache-manager (with redis store)Tiered cache library; uses one of the redis clients underneath.
Upstash Redis SDKHTTP-based for serverless / edge. Different protocol; not RESP.

Real-world recipes

Basic get / set

The client uses lazy connection — connect() opens the socket. All commands return Promises.

typescript
import { createClient } from "redis";

const client = createClient({ url: process.env.REDIS_URL });
client.on("error", (err) => console.error("redis", err));
await client.connect();

await client.set("greeting", "hello");
const value = await client.get("greeting");
console.log(value);

await client.quit();

Output: hello. quit() flushes and closes; disconnect() closes immediately.

Pipelining (multi / exec)

A pipeline batches commands into a single round trip. Use multi() for atomicity (a transaction) or client.commandsExecutor for naive batching.

typescript
import { createClient } from "redis";

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

const results = await client
  .multi()
  .set("user:1:name", "Alice")
  .set("user:1:email", "alice@example.com")
  .incr("user:count")
  .exec();

console.log(results);

await client.quit();

Output: ['OK', 'OK', 1]. All three commands run as a single transaction. If any command would fail at queue time, the entire transaction is aborted.

For non-transactional batching (no atomicity, just round-trip savings), use await Promise.all([...client.set(...)]) with the auto-pipelining the client does internally.

Pub/Sub

Pub/Sub needs separate clients for publishing and subscribing — once a connection enters subscribe mode, it accepts only SUBSCRIBE / UNSUBSCRIBE / PSUBSCRIBE / PUNSUBSCRIBE / QUIT.

typescript
import { createClient } from "redis";

const pub = createClient({ url: process.env.REDIS_URL });
const sub = pub.duplicate();
await pub.connect();
await sub.connect();

await sub.subscribe("events", (msg, channel) => {
  console.log(`[${channel}] ${msg}`);
});

await pub.publish("events", "user-signed-up:u-1");

setTimeout(async () => {
  await sub.unsubscribe();
  await pub.quit();
  await sub.quit();
}, 1000);

Output: [events] user-signed-up:u-1. Each subscriber receives the message asynchronously.

For at-least-once semantics, use Redis Streams instead of Pub/Sub — Streams persist messages and support consumer groups.

TTL / expiry

Redis can expire keys automatically — useful for sessions, rate limits, short-lived caches.

typescript
import { createClient } from "redis";

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

await client.set("session:abc", JSON.stringify({ uid: "u-1" }), {
  EX: 3600, // expire in 1 hour
});

const ttl = await client.ttl("session:abc");
console.log(`ttl=${ttl}s`); // ttl≈3600

await client.expire("session:abc", 60); // shorten to 60s
await client.quit();

Output: ttl=3600s. After 60 seconds the key vanishes. Negative ttl means no expiry; -2 means the key doesn't exist.

EX is seconds; PX is milliseconds; EXAT / PXAT are absolute UNIX times.

Rate-limit counter pattern

A common Redis use case is sliding-window or fixed-window rate limiting. Fixed-window is the simplest — INCR + EXPIRE per identifier per window.

typescript
import { createClient } from "redis";

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

async function allow(ip: string, limit = 60, windowSec = 60): Promise<boolean> {
  const key = `rl:${ip}:${Math.floor(Date.now() / 1000 / windowSec)}`;
  const count = await client.incr(key);
  if (count === 1) await client.expire(key, windowSec);
  return count <= limit;
}

console.log(await allow("1.2.3.4")); // true for first 60 requests in a window

Output: true for requests within the window's quota, false afterward. The key auto-expires when the window passes.

For production rate limiting, prefer rate-limiter-flexible — it implements sliding-window correctly via Lua scripts (no race between INCR and EXPIRE).

Production deployment

Connection management

The client reconnects automatically. Configure backoff via socket.reconnectStrategy:

typescript
const client = createClient({
  url: process.env.REDIS_URL,
  socket: {
    reconnectStrategy: (retries) => Math.min(retries * 50, 2000),
    connectTimeout: 5000,
    keepAlive: 5000,
  },
});

Output: retries with capped exponential backoff; 5-second connection timeout; TCP keepalive every 5s to detect dead peers.

Cluster client

For Redis Cluster, use createCluster instead of createClient:

typescript
import { createCluster } from "redis";

const cluster = createCluster({
  rootNodes: [
    { url: "redis://node1:6379" },
    { url: "redis://node2:6379" },
    { url: "redis://node3:6379" },
  ],
  defaults: {
    socket: { connectTimeout: 5000 },
  },
});
await cluster.connect();

Output: the client discovers the full topology from the root nodes and routes commands to the correct slot. Slot-spanning commands (MGET across multiple slots) fail unless every key is in the same hash slot — use hash tags ({user}:1, {user}:2) to group.

Sentinel

For sentinel-based HA, point the client at the sentinels:

typescript
const client = createClient({
  sentinels: [{ host: "sent1", port: 26379 }, { host: "sent2", port: 26379 }],
  name: "mymaster",
});

Output: client follows master failover automatically.

Memory monitoring

Run redis-cli INFO memory regularly. maxmemory + maxmemory-policy allkeys-lru (or volatile-lru) caps memory and evicts the least-recently-used keys. For cache workloads, allkeys-lru is the sane default.

ini
maxmemory 2gb
maxmemory-policy allkeys-lru

Output: Redis caps the dataset at 2 GB; oldest least-recently-used keys evict to stay under the cap.

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.

Performance tuning

  • Pipeline aggressively. Network RTT dominates. Batched multi() halves latency for any sequence of dependent commands.
  • Use EXPIRE not DEL for cleanup. Background expiry is cheaper than synchronous deletes.
  • Avoid KEYS. Blocks the server O(N) on the entire keyspace. Use SCAN with a cursor.
  • Hash keys to spread across slots in a cluster. MSET user:1 ... user:2 ... may cross-slot in a cluster; use {user:1} style hash-tags to colocate when you need atomic multi-key ops.
  • Use SETNX / SET ... NX for distributed locks — but prefer redlock for correct multi-master locking.
  • Compress large values. Strings >1 KB benefit from gzip; the network savings outpace CPU cost in most workloads.
  • CLIENT PAUSE for bulk loads. Pause writes briefly during a migration to keep the dataset consistent.
  • Reuse the client. Creating a new client per request kills performance — single long-lived client per process.

Version migration guide

From redis@4 to redis@5

Largely additive — type tightening and improved cluster API. Most projects upgrade with npm install redis@5 and tsc --noEmit.

  1. Some response types are narrower (e.g. GET returns string | null, not string | undefined).
  2. RESP3 by default when the server advertises it — affects clients that explicitly parsed OK strings.
  3. Cluster client gained nodeOptions for per-node overrides.
  4. Subscription callbacks: signature is (message, channel) — was (channel, message) in some early v4 drafts.

From redis@3 (callback) to redis@4 / redis@5

This is a big rewrite. Plan a focused migration window.

  1. API shape: client.set("k", "v", cb)await client.set("k", "v").
  2. No more set("k", "v", "EX", 3600, cb). Options are an object: client.set("k", "v", { EX: 3600 }).
  3. No more client.send_command. Every command is a method or accessible via client.sendCommand([...]).
  4. createClient(port, host, options)createClient({ url: "redis://host:port" }).
  5. Subscription API rewritten. client.on("message", cb) no longer works — pass a callback to client.subscribe(channel, cb).
  6. Error handling. Connection errors throw via the error event AND promise rejections. Add both handlers.
bash
npm install redis@5
# then search-and-replace callbacks → awaits in source

Output: redis@5 installed; remaining manual rewrite is converting callback-style calls to await.

Mid-migration, run both clients side-by-side: keep redis@3 for legacy code paths, mount redis@5 for new ones.

Security considerations

  • Always set requirepass / use ACLs. Default Redis has no auth. Set via AUTH or the password: field in the URL.
  • TLS for any network-traversing Redis. Pass socket: { tls: true, rejectUnauthorized: true, ca: ... }. Self-hosted Redis 6+ supports TLS natively.
  • Disable dangerous commands in production (FLUSHDB, FLUSHALL, DEBUG, CONFIG) via rename-command in redis.conf or ACLs.
  • Network isolation. Redis should NEVER face the public internet. Bind to private subnet only.
  • SETNX is not enough for distributed locks. Use redlock with the canonical algorithm.
  • Don't store secrets unencrypted. Redis keys are visible to anyone with auth; encrypt sensitive payloads client-side.
  • Cap key sizes / value sizes. Large keys (> 1 MB) drag the server. Validate before SET.
  • maxmemory-policy noeviction causes hard OOM. Use an eviction policy for caching workloads.
  • Audit pub/sub messages. Untrusted publishers can inject any payload — validate downstream.

Testing & CI integration

For unit tests:

  • ioredis-mock — pure-JS in-memory Redis-compatible stub (works with both ioredis and limited redis@4 usage).
  • testcontainers — real Redis in Docker; full fidelity.
  • GitHub Actions service container — fastest CI path.
yaml
services:
  redis:
    image: redis:7
    ports: ["6379:6379"]
    options: >-
      --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5

For integration tests with redis@5:

typescript
import { createClient } from "redis";
import { beforeAll, afterAll, beforeEach } from "vitest";

let client: ReturnType<typeof createClient>;

beforeAll(async () => {
  client = createClient({ url: "redis://localhost:6379" });
  await client.connect();
});

beforeEach(async () => {
  await client.flushDb();
});

afterAll(async () => {
  await client.quit();
});

Output: clean Redis state per test; quick suite teardown.

Ecosystem integrations

ToolRole
connect-redisExpress session store.
bullmqJob queue on top of Redis.
rate-limiter-flexibleSliding-window rate limiting.
socket.io-redis (now @socket.io/redis-adapter)Cross-instance Socket.IO state.
cache-manager (with redis store)Tiered cache abstraction.
keyv (with @keyv/redis)Key-value abstraction over Redis.
redlockDistributed locking.
OpenTelemetry redis instrumentationSpans for each command.
redis-omObject-mapping over RedisJSON + RediSearch.

Troubleshooting common errors

Connection is closed — using the client after quit() / disconnect(). Verify lifecycle.

NOAUTH Authentication required — Redis has requirepass set but the URL didn't include the password. Use redis://:password@host:6379 or pass password: in the options.

WRONGTYPE Operation against a key holding the wrong kind of value — using a string command on a hash (or vice versa). Inspect with TYPE <key>.

READONLY You can't write against a read only replica — wrote to a replica node. Set readOnly: false on the cluster client to route writes to masters.

MOVED <slot> <addr> (cluster) — non-cluster client used against a cluster. Switch to createCluster.

CLUSTERDOWN Hash slot not served — cluster is in a failover / resharding state. Retry with backoff.

Memory leak — usually a forgotten EXPIRE or unbounded LPUSH. Audit INFO memory and the biggest keys via redis-cli --bigkeys.

Slow KEYS * — never run this in production. Replace with SCAN.

Subscriber connection blocked — once in subscribe mode, the connection refuses other commands. Use client.duplicate() for a dedicated subscriber.

MaxListenersExceededWarning — too many client.on('message', ...) handlers attached. Audit duplicate subscribe calls.

When NOT to use this

  • You need a primary data store, not a cache. Redis is durable with AOF/RDB but losing the last second of writes is acceptable in cache mode, not in OLTP. Use Postgres / Mongo as the system of record.
  • Edge runtimes. Node-redis uses TCP sockets — won't run on Cloudflare Workers / Vercel Edge. Use Upstash REST or Cloudflare KV / Durable Objects instead.
  • You already use ioredis. Don't mix both clients in one app — pick one. ioredis is more battle-tested for cluster/sentinel; node-redis is upstream-official and type-first.
  • Vector search at scale. Redis Search vector indexes are improving but vector DBs (Qdrant, Milvus, pgvector) often outperform them.
  • Pub/Sub with delivery guarantees. Pub/Sub is fire-and-forget — late subscribers miss messages. Use Redis Streams or a real broker (NATS, Kafka).

See also