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
npm install redis
Output: added redis to dependencies
pnpm add redis
Output: added 1 package, linked from store
yarn add redis
Output: added redis
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 storerate-limiter-flexible— rate-limit primitives with Redis backendbullmq— job queue on Redisredlock— distributed lock implementation
Alternatives
| Client | Trade-off |
|---|---|
ioredis | Older, very mature, robust cluster + sentinel support. Different API surface. |
tedis / denoland/redis | Smaller / 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 SDK | HTTP-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.
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.
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.
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.
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.
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:
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:
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:
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.
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
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
EXPIREnotDELfor cleanup. Background expiry is cheaper than synchronous deletes. - Avoid
KEYS. Blocks the server O(N) on the entire keyspace. UseSCANwith 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 ... NXfor distributed locks — but preferredlockfor correct multi-master locking. - Compress large values. Strings >1 KB benefit from gzip; the network savings outpace CPU cost in most workloads.
CLIENT PAUSEfor 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.
- Some response types are narrower (e.g.
GETreturnsstring | null, notstring | undefined). - RESP3 by default when the server advertises it — affects clients that explicitly parsed
OKstrings. - Cluster client gained
nodeOptionsfor per-node overrides. - 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.
- API shape:
client.set("k", "v", cb)→await client.set("k", "v"). - No more
set("k", "v", "EX", 3600, cb). Options are an object:client.set("k", "v", { EX: 3600 }). - No more
client.send_command. Every command is a method or accessible viaclient.sendCommand([...]). createClient(port, host, options)→createClient({ url: "redis://host:port" }).- Subscription API rewritten.
client.on("message", cb)no longer works — pass a callback toclient.subscribe(channel, cb). - Error handling. Connection errors throw via the
errorevent AND promise rejections. Add both handlers.
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 viaAUTHor thepassword: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) viarename-commandinredis.confor ACLs. - Network isolation. Redis should NEVER face the public internet. Bind to private subnet only.
SETNXis not enough for distributed locks. Useredlockwith 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 noevictioncauses 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 bothioredisand limitedredis@4usage).- testcontainers — real Redis in Docker; full fidelity.
- GitHub Actions service container — fastest CI path.
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:
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
| Tool | Role |
|---|---|
connect-redis | Express session store. |
bullmq | Job queue on top of Redis. |
rate-limiter-flexible | Sliding-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. |
redlock | Distributed locking. |
OpenTelemetry redis instrumentation | Spans for each command. |
redis-om | Object-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.ioredisis more battle-tested for cluster/sentinel;node-redisis 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
- Redis package: ioredis — alternative client with cluster/sentinel focus
- Concept: async — promises and event-driven patterns
- Concept: api — protocol vs SDK boundaries