1. Project Overview & Quickstart (redis/node-redis)
File: README.md
Node-Redis
node-redis is a modern, high performance Redis client for Node.js.
How do I Redis?
Learn for free at Redis University
Build faster with the Redis Launchpad
Installation
Start a redis via docker:
docker run -p 6379:6379 -d redis:8.0-rc1To install node-redis, simply:
npm install redis"redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands,
you can install the individual packages. See the list below.
Packages
| Name | Description |
|---|---|
redis |
The client with all the "redis-stack" modules |
@redis/client |
The base clients (i.e RedisClient, RedisCluster, etc.) |
@redis/bloom |
Redis Bloom commands |
@redis/json |
Redis JSON commands |
@redis/search |
RediSearch commands |
@redis/time-series |
Redis Time-Series commands |
@redis/entraid |
Secure token-based authentication for Redis clients using Microsoft Entra ID |
Looking for a high-level library to handle object mapping?
See redis-om-node!
Usage
Basic Example
import { createClient } from "redis";
const client = await createClient()
.on("error", (err) => console.log("Redis Client Error", err))
.connect();
await client.set("key", "value");
const value = await client.get("key");
console.log(value); // 'value'
client.destroy();The same example in CommonJS (for example node example.js without "type": "module" in package.json):
const { createClient } = require("redis");
async function main() {
const client = await createClient()
.on("error", (err) => console.log("Redis Client Error", err))
.connect();
await client.set("key", "value");
const value = await client.get("key");
console.log(value); // 'value'
client.destroy();
}
main().catch(console.error);The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in
the format redis[s]://[[username][:password]@][host][:port][/db-number]:
createClient({
url: "redis://alice:[email protected]:6380",
});You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in
the client configuration guide.
To check if the client is connected and ready to send commands, use client.isReady which returns a boolean.client.isOpen is also available. This returns true when the client's underlying socket is open, and false when it
isn't (for example when the client is still connecting or reconnecting after a network error).
Redis Commands
There is built-in support for all of the out-of-the-box Redis commands. They are exposed
using the raw Redis command names (HSET, HGETALL, etc.) and a friendlier camel-cased version (hSet, hGetAll,
etc.):
// raw Redis commands
await client.HSET("key", "field", "value");
await client.HGETALL("key");
// friendly JavaScript commands
await client.hSet("key", "field", "value");
await client.hGetAll("key");Modifiers to commands are specified using a JavaScript object:
await client.set("key", "value", {
EX: 10,
NX: true,
});Replies will be transformed into useful data structures:
await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' }
await client.hVals("key"); // ['value1', 'value2']Buffers are supported as well:
const client = createClient().withTypeMapping({
[RESP_TYPES.BLOB_STRING]: Buffer
});
await client.hSet("key", "field", Buffer.from("value")); // 'OK'
await client.hGet("key", "field"); // { field: }For commands that return serialized binary payloads, such as DUMP, map blob strings to Buffer before using the result with commands like RESTORE:
const binaryClient = createClient().withTypeMapping({
[RESP_TYPES.BLOB_STRING]: Buffer
});
const dump = await binaryClient.dump("source");
await binaryClient.restore("destination", 0, dump);Unsupported Redis Commands
If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use .sendCommand():
await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK'
await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2']Note: the API is different when using a cluster.
Transactions (Multi/Exec)
Start a transaction by calling .multi(), then chaining your commands. When
you're done, call .exec() and you'll get an array back with your results:
await client.set("another-key", "another-value");
const [setKeyReply, otherKeyValue] = await client
.multi()
.set("key", "value")
.get("another-key")
.exec(); // ['OK', 'another-value']You can also watch keys by calling.watch(). Your transaction will abort if any of the watched keys change.
Blocking Commands
In v4, RedisClient had the ability to create a pool of connections using an "Isolation Pool" on top of the "main"
connection. However, there was no way to use the pool without a "main" connection:
const client = await createClient()
.on("error", (err) => console.error(err))
.connect();
await client.ping(client.commandOptions({ isolated: true }));In v5 we've extracted this pool logic into its own classโRedisClientPool:
const pool = await createClientPool()
.on("error", (err) => console.error(err))
.connect();
await pool.ping();Pub/Sub
See the Pub/Sub overview.
Scan Iterator
SCAN results can be looped over
using async iterators:
for await (const keys of client.scanIterator()) {
console.log(keys, await client.mGet(keys));
}This works with HSCAN, SSCAN, and ZSCAN too:
for await (const { field, value } of client.hScanIterator("hash")) {
}
for await (const member of client.sScanIterator("set")) {
}
for await (const { score, value } of client.zScanIterator("sorted-set")) {
}You can override the default options by providing a configuration object:
client.scanIterator({
TYPE: "string", // `SCAN` only
MATCH: "pattern*",
COUNT: 100,
});Compare-and-Set/Delete (CAS/CAD)
Note: CAS/CAD operations were introduced in Redis 8.4
// Conditionally update only if the current value matches
await client.set("key", "new-value", { condition: "IFEQ", matchValue: "old-value" });
// Conditionally delete only if the current value matches
await client.delEx("key", { condition: "IFEQ", matchValue: "expected-value" });Local Digest
Note: This feature requires the optional
@node-rs/xxhashpeer dependency.
The digest helper computes an XXH3 64-bit hash locally, matching what Redis computes via the DIGEST command. This is useful for CAS/CAD operations with large values where comparing the full value would be inefficient.
npm install @node-rs/xxhashimport { digest } from "redis";
const hash = await digest("my-value");
await client.set("key", "new-value", { condition: "IFDEQ", matchValue: hash });Disconnecting
The QUIT command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead
of sending a QUIT command to the server, the client can simply close the network connection.
client.QUIT/quit() is replaced by client.close(). and, to avoid confusion, client.disconnect() has been renamed toclient.destroy().
client.destroy();Client Side Caching
Node Redis v5 adds support for Client Side Caching, which enables clients to cache query results locally. The Redis server will notify the client when cached results are no longer valid.
// Enable client side caching with RESP3
const client = createClient({
RESP: 3,
clientSideCache: {
ttl: 0, // Time-to-live (0 = no expiration)
maxEntries: 0, // Maximum entries (0 = unlimited)
evictPolicy: "LRU" // Eviction policy: "LRU" or "FIFO"
}
});See the V5 documentation for more details and advanced usage.
Auto-Pipelining
Node Redis will automatically pipeline requests that are made during the same "tick".
client.set("Tm9kZSBSZWRpcw==", "users:1");
client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw==");Of course, if you don't do something with your Promises you're certain to
get unhandled Promise exceptions. To take
advantage of auto-pipelining and handle your Promises, use Promise.all().
await Promise.all([
client.set("Tm9kZSBSZWRpcw==", "users:1"),
client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="),
]);Sentinel
Look at the sentinel section to figure out how to use this library with sentinels.
Programmability
See the Programmability overview.
Clustering
Check out the Clustering Guide when using Node Redis to connect to a Redis Cluster.
OpenTelemetry
OpenTelemetry Metrics Instrumentation
import { createClient, OpenTelemetry } from "redis";
OpenTelemetry.init({
metrics: {
enabled: true
}
});
const client = createClient()
await client.connect();
// ... use the client as usualImportant: Initializing OpenTelemetry only enables node-redis metrics instrumentation and requires both @opentelemetry/api and an OpenTelemetry SDK configured in your application.
Important: Initialize OpenTelemetry before creating Redis clients.
For SDK/provider/exporter setup, verification, and advanced configuration, see:
Diagnostics Channel
Node Redis publishes telemetry through Node.js diagnostics_channel, enabling APM tools and custom instrumentation to observe commands, connections, and internal events. See the Diagnostics Channel guide for the full channel reference and usage examples.
Events
The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes:
| Name | When | Listener arguments |
|---|---|---|
connect |
Initiating a connection to the server | No arguments |
ready |
Client is ready to use | No arguments |
end |
Connection has been closed (via .close() or .destroy()) |
No arguments |
error |
An error has occurredโusually a network issue such as "Socket closed unexpectedly" | (error: Error) |
reconnecting |
Client is trying to reconnect to the server | No arguments |
sharded-channel-moved |
See here | See here |
invalidate |
Client Tracking is on with emitInvalidate and a key is invalidated |
(key: RedisItem | null) |
:warning: You MUST listen to
errorevents. If a client doesn't have at least oneerrorlistener registered and
anerroroccurs, that error will be thrown and the Node.js process will exit. See the >EventEmitterdocs for more details.
The client will not emit any other events beyond those listed above.
Supported Redis versions
Node Redis is supported with the following versions of Redis:
Migration
Contributing
If you'd like to contribute, check out the contributing guide.
Thank you to all the people who already contributed to Node Redis!
License
This repository is licensed under the "MIT" license. See LICENSE.
File: docs/client-configuration.md
`createClient` configuration
| Property | Default | Description |
|---|---|---|
| url | redis[s]://[[username][:password]@][host][:port][/db-number] (see redis and rediss IANA registration for more details), or unix://[[username][:password]@]/path/to/socket[?db=N] for a UNIX domain socket |
|
| socket | Socket connection properties. Unlisted net.connect properties (and tls.connect) are also supported |
|
| socket.port | 6379 |
Redis server port |
| socket.host | 'localhost' |
Redis server hostname |
| socket.servername | Server name for the SNI (Server Name Indication) TLS extension. Set this property if the Redis server requires SNI during TLS handshake. | |
| socket.family | 0 |
IP Stack version (one of 4 | 6 | 0) |
| socket.path | Path to the UNIX Socket | |
| socket.connectTimeout | 5000 |
Connection timeout (in milliseconds) |
| socket.socketTimeout | The maximum duration (in milliseconds) that the socket can remain idle (i.e., with no data sent or received) before being automatically closed | |
| socket.noDelay | true |
Toggle Nagle's algorithm |
| socket.keepAlive | true |
Toggle keep-alive functionality |
| socket.keepAliveInitialDelay | 30000 |
If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket |
| socket.tls | See explanation and examples below | |
| socket.reconnectStrategy | Exponential backoff with a maximum of 2000 ms; plus 0-200 ms random jitter. | A function containing the Reconnect Strategy logic |
| username | ACL username (see ACL guide) | |
| password | ACL password or the old "--requirepass" password | |
| name | Client name (see CLIENT SETNAME) |
|
| database | Redis database number (see SELECT command) |
|
| keyPrefix | Prefix prepended to every key sent to Redis (ioredis-compatible). See Key Prefixing. | |
| modules | Included Redis Modules | |
| scripts | Script definitions (see Lua Scripts) | |
| functions | Function definitions (see Functions) | |
| commandsQueueMaxLength | Maximum length of the client's internal command queue | |
| disableOfflineQueue | false |
Disables offline queuing, see FAQ |
| readonly | false |
Connect in READONLY mode |
| legacyMode | false |
Maintain some backwards compatibility (see the Migration Guide) |
| isolationPoolOptions | An object that configures a pool of isolated connections, If you frequently need isolated connections, consider using createClientPool instead | |
| pingInterval | Send PING command at interval (in ms). Useful with "Azure Cache for Redis" |
|
| disableClientInfo | false |
Disables CLIENT SETINFO LIB-NAME node-redis and CLIENT SETINFO LIB-VER X.X.X commands |
| commandOptions.timeout | 5000 |
Default per-command timeout in milliseconds. Set to undefined (or 0) to disable. See Command Options. |
Reconnect Strategy
When the socket closes unexpectedly (without calling .quit()/.disconnect()), the client uses reconnectStrategy to decide what to do. The following values are supported:
false-> do not reconnect, close the client and flush the command queue.number-> wait forXmilliseconds before reconnecting.(retries: number, cause: Error) => false | number | Error->numberis the same as configuring anumberdirectly,Erroris the same asfalse, but with a custom error.
By default the strategy uses exponential backoff, but it can be overwritten like so:
createClient({
socket: {
reconnectStrategy: (retries, cause) => {
// By default, do not reconnect on socket timeout.
if (cause instanceof SocketTimeoutError) {
return false;
}
// Generate a random jitter between 0 โ 200 ms:
const jitter = Math.floor(Math.random() * 200);
// Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:
const delay = Math.min(Math.pow(2, retries) * 50, 2000);
return delay + jitter;
}
}
});TLS
To enable TLS, set socket.tls to true. Below are some basic examples.
For configuration options see tls.connect and tls.createSecureContext, as those are the underlying functions used by this library.
Create a SSL client
createClient({
socket: {
tls: true,
ca: '...',
cert: '...'
}
});Create a SSL client using a self-signed certificate
createClient({
socket: {
tls: true,
rejectUnauthorized: false,
cert: '...'
}
});Key Prefixing
The keyPrefix option prepends a prefix to every key sent to Redis. It is an ioredis-compatible
way to isolate keyspaces โ for example to isolate tests in CI, or to separate the keys of different
parts of an application (web app, background workers, โฆ) that share a single Redis instance.
const client = createClient({ keyPrefix: 'app:' });
await client.connect();
await client.set('key', 'value'); // actually stores 'app:key'
await client.get('key'); // reads 'app:key' -> 'value'The prefix is applied uniformly across the standard client, cluster,
sentinel, pool, and inside transactions and pipelines.
In cluster mode the slot is computed from the prefixed key, so routing remains correct.
Semantics (matching ioredis)
- Only keys sent to Redis are prefixed. Keys returned by Redis are not un-prefixed โ e.g.
KEYS *,SCAN, andRANDOMKEYreturn keys that still include the prefix. - Because returned keys keep the prefix,
scanIteratoryields already-prefixed keys. Feeding
them straight back into a key-prefixing command double-prefixes them โ e.g. withkeyPrefix: 'app:',
a yielded'app:foo'passed toclient.mGet(...)becomes'app:app:foo'. Strip the prefix first,
or use a client without akeyPrefix. See Scan Iterators. SCAN/KEYS/HSCAN/โฆMATCHpatterns are not auto-prefixed. Include the prefix in the
pattern yourself if required (e.g.client.scan('0', { MATCH: 'app:user:*' })).- Pub/Sub channels are not prefixed (this includes sharded
SPUBLISH/SSUBSCRIBE), since
channels are a separate namespace from keys. - The deprecated
parseArgs/transformArgumentshelper does not applykeyPrefix.
keyPrefix may be a string or a Buffer.
Connection Pooling
In most cases, a single Redis connection is sufficient, as the node-redis client efficiently handles commands using an underlying socket. Unlike traditional databases, Redis does not require connection pooling for optimal performance.
However, if your use case requires exclusive connections see RedisClientPool, which allows you to create and manage multiple dedicated connections.
File: docs/clustering.md
Clustering
Basic Example
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
import { createCluster } from 'redis';
const cluster = await createCluster({
rootNodes: [{
url: 'redis://10.0.0.1:30001'
}, {
url: 'redis://10.0.0.2:30002'
}]
})
.on('error', err => console.log('Redis Cluster Error', err))
.connect();
await cluster.set('key', 'value');
const value = await cluster.get('key');
await cluster.close();`createCluster` configuration
See the client configuration page for the
rootNodesanddefaultsconfiguration schemas.
| Property | Default | Description |
|---|---|---|
| rootNodes | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the cluster configuration from the server | |
| defaults | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with | |
| useReplicas | false |
When true, distribute load by executing readonly commands (such as GET, GEOSEARCH, etc.) across all cluster nodes. When false, only use master nodes |
| minimizeConnections | false |
When true, .connect() will only discover the cluster topology, without actually connecting to all the nodes. Useful for short-term or Pub/Sub-only connections. |
| maxCommandRedirections | 16 |
The maximum number of times a command will be redirected due to MOVED or ASK errors |
| nodeAddressMap | Defines the node address mapping | |
| modules | Included Redis Modules | |
| scripts | Script definitions (see Lua Scripts) | |
| functions | Function definitions (see Functions) |
Usage
Most redis commands are the same as with individual clients.
Unsupported Redis Commands
If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use .sendCommand().
When clustering, sendCommand takes 3 arguments to help with routing to the correct redis node:
firstKey: the key that is being operated on, orundefinedto route to a random node.isReadOnly: determines if the command needs to go to the master or may go to a replica.args: the command and all arguments (including the key), as an array of strings.
await cluster.sendCommand("key", false, ["SET", "key", "value", "NX"]); // 'OK'
await cluster.sendCommand("key", true, ["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2']Root nodes vs. defaults
The configuration in rootNodes is only used for the connections that discover the cluster topology. It is not inherited by the connections the cluster then makes to the discovered nodes โ those are created from the defaults option (plus the discovered host and port).
This means that any setting that should apply to every connection in the cluster โ credentials, TLS, timeouts, etc. โ must be specified via defaults, even if it is already present in a root node URL or configuration.
Auth with password and username
Specifying the password in the URL or a root node will only affect the connection used for topology discovery. In case you want to set the password for all the connections being created from a cluster instance, use the defaults option.
createCluster({
rootNodes: [{
url: 'redis://10.0.0.1:30001'
}, {
url: 'redis://10.0.0.2:30002'
}],
defaults: {
username: 'username',
password: 'password'
}
});TLS
Likewise, TLS options specified in the URL (rediss://) or in a root node only affect the topology discovery connection. If your cluster requires TLS (e.g. AWS ElastiCache with in-transit encryption enabled), enable it via defaults.socket, otherwise the connections to the discovered nodes will be attempted in plaintext and can hang without an error:
createCluster({
rootNodes: [{
url: 'rediss://external-host.io:30001'
}],
defaults: {
socket: {
tls: true
}
}
});Node Address Map
A mapping between the addresses in the cluster (see CLUSTER SHARDS) and the addresses the client should connect to.
Useful when the cluster is running on a different network to the client.
const rootNodes = [{
url: 'external-host-1.io:30001'
}, {
url: 'external-host-2.io:30002'
}];
// Use either a static mapping:
createCluster({
rootNodes,
nodeAddressMap: {
'10.0.0.1:30001': {
host: 'external-host.io',
port: 30001
},
'10.0.0.2:30002': {
host: 'external-host.io',
port: 30002
}
}
});
// or create the mapping dynamically, as a function:
createCluster({
rootNodes,
nodeAddressMap(address) {
const indexOfDash = address.lastIndexOf('-'),
indexOfDot = address.indexOf('.', indexOfDash),
indexOfColons = address.indexOf(':', indexOfDot);
return {
host: `external-host-${address.substring(indexOfDash + 1, indexOfDot)}.io`,
port: Number(address.substring(indexOfColons + 1))
};
}
});This is a common problem when using ElastiCache. See Accessing ElastiCache from outside AWS for more information on that.
Events
The Node Redis Cluster class extends Node.jsโs EventEmitter and emits the following events:
| Name | When | Listener arguments |
|---|---|---|
connect |
The cluster has successfully connected and is ready to us | No arguments |
disconnect |
The cluster has disconnected | No arguments |
error |
The cluster has errored | (error: Error) |
node-ready |
A cluster node is ready to establish a connection | (node: { host: string, port: number }) |
node-connect |
A cluster node has connected | (node: { host: string, port: number }) |
node-reconnecting |
A cluster node is attempting to reconnect after an error | (node: { host: string, port: number }) |
node-disconnect |
A cluster node has disconnected | (node: { host: string, port: number }) |
node-error |
A cluster node has has errored (usually during TCP connection) | (error: Error, node: { host: string, port: number }) |
:warning: You MUST listen to
errorevents. If a cluster doesn't have at least oneerrorlistener registered and
anerroroccurs, that error will be thrown and the Node.js process will exit. See the >EventEmitterdocs for more details.
Command Routing
Commands that operate on Redis Keys
Commands such as GET, SET, etc. are routed by the first key specified. For example MGET 1 2 3 will be routed by the key 1.
[Server Commands](https://redis.io/commands#server)
Admin commands such as MEMORY STATS, FLUSHALL, etc. are not attached to the cluster, and must be executed on a specific node via .getSlotMaster().
"Forwarded Commands"
Certain commands (e.g. PUBLISH) are forwarded to other cluster nodes by the Redis server. The client sends these commands to a random node in order to spread the load across the cluster.
Transactions with `WATCH`
WATCH relies on connection-level state on a specific node, so it isn't exposed directly on the cluster client. Use .getNodeClientForKey() to get the node client responsible for a key's slot and run the optimistic-locking transaction on it:
const key = 'key';
const nodeClient = await cluster.getNodeClientForKey(key);
await nodeClient.watch(key);
const value = await nodeClient.get(key);
const reply = await nodeClient
.multi()
.set(key, calculateNewValue(value)) // application logic
.exec(); // `null` if `key` changed since `WATCH`, retry in that caseAll keys touched in the transaction must hash to the same slot. Pass true as the second argument (getNodeClientForKey(key, true)) to allow a replica for read-only use.
File: docs/command-options.md
Command Options
:warning: The command options API in v5 has breaking changes from the previous version. For more details, refer to the v4-to-v5 guide.
Command Options are used to create "proxy clients" that change the behavior of executed commands. See the sections below for details.
Type Mapping
Some RESP types can be mapped to more than one JavaScript type. For example, "Blob String" can be mapped to string or Buffer. You can override the default type mapping using the withTypeMapping function:
await client.get('key'); // `string | null`
const proxyClient = client.withTypeMapping({
[TYPES.BLOB_STRING]: Buffer
});
await proxyClient.get('key'); // `Buffer | null`See RESP for a full list of types.
`DUMP`/`RESTORE` and Binary Data
DUMP returns serialized binary data. If blob strings are decoded as string (the default), the payload can be lossy and RESTORE may fail with ERR DUMP payload version or checksum are wrong.
Use Buffer mapping for blob strings:
import { createClient, RESP_TYPES } from 'redis';
const client = createClient().withTypeMapping({
[RESP_TYPES.BLOB_STRING]: Buffer
});
const dump = await client.dump('source');
await client.restore('destination', 0, dump);You can also set it as a default:
const client = createClient({
commandOptions: {
typeMapping: {
[RESP_TYPES.BLOB_STRING]: Buffer
}
}
});In v5, use withTypeMapping or createClient({ commandOptions: { typeMapping } }) instead of the v4 client.commandOptions({ returnBuffers: true }) call style.
Abort Signal
The client batches commands before sending them to Redis. Commands that haven't been written to the socket yet can be aborted using the AbortSignal API:
const controller = new AbortController(),
client = client.withAbortSignal(controller.signal);
try {
const promise = client.get('key');
controller.abort();
await promise;
} catch (err) {
// AbortError
}Timeout
This option is similar to the Abort Signal one, but provides an easier way to set timeout for commands. Again, this applies to commands that haven't been written to the socket yet.
Since v6, commandOptions.timeout defaults to 5000 (5 seconds). Set it to undefined (or 0) to disable.
const client = createClient({
commandOptions: {
timeout: 1000
}
})ASAP
Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue.
const asapClient = client.asap();
await asapClient.ping();`withCommandOptions`
You can set all of the above command options in a single call with the withCommandOptions function:
client.withCommandOptions({
typeMapping: ...,
abortSignal: ...,
asap: ...
});If any of the above options are omitted, the default value will be used. For example, the following client would not be in ASAP mode:
client.asap().withCommandOptions({
typeMapping: ...,
abortSignal: ...
});File: docs/FAQ.md
F.A.Q.
Nobody has actually asked these questions. But, we needed somewhere to put all the important bits and bobs that didn't fit anywhere else. So, here you go!
What happens when the network goes down?
When a socket closes unexpectedly, all the commands that were already sent will reject as they might have been executed on the server. The rest will remain queued in memory until a new socket is established. If the client is closedโeither by returning an error from reconnectStrategy or by manually calling .disconnect()โthey will be rejected.
If don't want to queue commands in memory until a new socket is established, set the disableOfflineQueue option to true in the client configuration. This will result in those commands being rejected.
How are commands batched?
Commands are pipelined using setImmediate.
If socket.write() returns falseโmeaning that "all or part of the data was queued in user memory"โthe commands will stack in memory until the drain event is fired.
`RedisClientType`
Redis has support for modules and running Lua scripts within the Redis context. To take advantage of typing within these scenarios, RedisClient and RedisCluster should be used with typeof, rather than the base types RedisClientType and RedisClusterType.
import { createClient } from '@redis/client';
export const client = createClient();
export type RedisClientType = typeof client;File: docs/pool.md
`RedisClientPool`
Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
- You want to run a blocking command that will take over the connection, such as
BLPOPorBLMOVE. - You're using transactions and need to
WATCHa key or keys for changes. - Some more...
For those use cases you'll need to create a connection pool.
Creating a pool
You can create a pool using the createClientPool function:
import { createClientPool } from 'redis';
const pool = await createClientPool()
.on('error', err => console.error('Redis Client Pool Error', err));the function accepts two arguments, the client configuration (see here for more details), and the pool configuration:
| Property | Default | Description |
|---|---|---|
| minimum | 1 | The minimum clients the pool should hold to. The pool won't close clients if the pool size is less than the minimum. |
| maximum | 100 | The maximum clients the pool will have at once. The pool won't create any more resources and queue requests in memory. |
| acquireTimeout | 3000 | The maximum time (in ms) a task can wait in the queue. The pool will reject the task with TimeoutError in case of a timeout. |
| cleanupDelay | 3000 | The time to wait before cleaning up unused clients. |
You can also create a pool from a client (reusing its configuration):
const pool = await client.createPool()
.on('error', err => console.error('Redis Client Pool Error', err));The Simple Scenario
All the client APIs are exposed on the pool instance directly, and will execute the commands using one of the available clients.
await pool.sendCommand(['PING']); // 'PONG'
await client.ping(); // 'PONG'
await client.withTypeMapping({
[RESP_TYPES.SIMPLE_STRING]: Buffer
}).ping(); // BufferTransactions
Things get a little more complex with transactions. Here we are .watch()ing some keys. If the keys change during the transaction, a WatchError is thrown when .exec() is called:
try {
await pool.execute(async client => {
await client.watch('key');
const multi = client.multi()
.ping()
.get('key');
if (Math.random() > 0.5) {
await client.watch('another-key');
multi.set('another-key', await client.get('another-key') / 2);
}
return multi.exec();
});
} catch (err) {
if (err instanceof WatchError) {
// the transaction aborted
}
}File: docs/programmability.md
[Programmability](https://redis.io/docs/manual/programmability/)
Redis provides a programming interface allowing code execution on the redis server.
[Functions](https://redis.io/docs/manual/programmability/functions-intro/)
The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key foo has the value 17 and we run add('foo', 25), it returns the answer to Life, the Universe and Everything.
#!lua name=library
redis.register_function {
function_name = 'add',
callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end,
flags = { 'no-writes' }
}Here is the same example, but in a format that can be pasted into the redis-cli.
FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={'no-writes'}}"Load the prior redis function on the redis server before running the example below.
import { CommandParser, createClient, RedisArgument } from '@redis/client';
import { NumberReply } from '@redis/client/dist/lib/RESP/types.js';
const client = createClient({
functions: {
library: {
add: {
NUMBER_OF_KEYS: 1,
parseCommand(
parser: CommandParser,
key: RedisArgument,
toAdd: RedisArgument
) {
parser.pushKey(key)
parser.push(toAdd)
},
transformReply: undefined as unknown as () => NumberReply
}
}
}
});
await client.connect();
await client.set('key', '1');
await client.library.add('key', '2'); // 3[Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/)
The following is an end-to-end example of the prior concept.
import { CommandParser, createClient, defineScript, RedisArgument } from '@redis/client';
import { NumberReply } from '@redis/client/dist/lib/RESP/types.js';
const client = createClient({
scripts: {
add: defineScript({
SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];',
NUMBER_OF_KEYS: 1,
FIRST_KEY_INDEX: 1,
parseCommand(
parser: CommandParser,
key: RedisArgument,
toAdd: RedisArgument
) {
parser.pushKey(key)
parser.push(toAdd)
},
transformReply: undefined as unknown as () => NumberReply
})
}
});
await client.connect();
await client.set('key', '1');
await client.add('key', '2'); // 3File: docs/pub-sub.md
Pub/Sub
The Pub/Sub API is implemented by RedisClient, RedisCluster, and RedisSentinel.
Pub/Sub with `RedisClient`
RESP2
Using RESP2, Pub/Sub "takes over" the connection (a client with subscriptions will not execute commands), therefore it requires a dedicated connection. You can easily get one by .duplicate()ing an existing RedisClient:
const subscriber = client.duplicate();
subscriber.on('error', err => console.error(err));
await subscriber.connect();When working with either
RedisClusterorRedisSentinel, this is handled automatically for you.
`sharded-channel-moved` event
RedisClient emits the sharded-channel-moved event when the "cluster slot" of a subscribed Sharded Pub/Sub channel has been moved to another shard.
The event listener signature is as follows:
(
channel: string,
listeners: {
buffers: Set<Listener>;
strings: Set<Listener>;
}
)When working with
RedisCluster, this is handled automatically for you.
Subscribing
const listener = (message, channel) => console.log(message, channel);
await client.subscribe('channel', listener);
await client.pSubscribe('channe*', listener);
// Use sSubscribe for sharded Pub/Sub:
await client.sSubscribe('channel', listener);โ ๏ธ Subscribing to the same channel more than once will create multiple listeners, each of which will be called when a message is received.
Publishing
await client.publish('channel', 'message');
// Use sPublish for sharded Pub/Sub:
await client.sPublish('channel', 'message');Unsubscribing
The code below unsubscribes all listeners from all channels.
await client.unsubscribe();
await client.pUnsubscribe();
// Use sUnsubscribe for sharded Pub/Sub:
await client.sUnsubscribe();To unsubscribe from specific channels:
await client.unsubscribe('channel');
await client.unsubscribe(['1', '2']);To unsubscribe a specific listener:
await client.unsubscribe('channel', listener);Buffers
Publishing and subscribing using Buffers is also supported:
await subscriber.subscribe('channel', message => {
console.log(message); //
}, true); // true = subscribe in `Buffer` mode.
await subscriber.publish(Buffer.from('channel'), Buffer.from('message'));NOTE: Buffers and strings are supported both for the channel name and the message. You can mix and match these as desired.
File: docs/RESP.md
Mapping RESP types
RESP, which stands for Redis SErialization Protocol, is the protocol used by Redis to communicate with clients. This document shows how RESP types can be mapped to JavaScript types. You can learn more about RESP itself in the official documentation.
By default, each type is mapped to the first option in the lists below. To change this, configure a typeMapping.
RESP2
- Integer (
:) =>number - Simple String (
+) =>string | Buffer - Blob String (
$) =>string | Buffer - Simple Error (
-) =>ErrorReply - Array (
*) =>Array
NOTE: the first type is the default type
RESP3
- Null (
_) =>null - Boolean (
#) =>boolean - Number (
:) =>number | string - Big Number (
() =>BigInt | string - Double (
,) =>number | string - Simple String (
+) =>string | Buffer - Blob String (
$) =>string | Buffer - Verbatim String (
=) =>string | Buffer | VerbatimString - Simple Error (
-) =>ErrorReply - Blob Error (
!) =>ErrorReply - Array (
*) =>Array - Set (
~) =>Array | Set - Map (
%) =>object | Map | Array - Push (
>) =>Array=> PubSub push/'push'event
NOTE: the first type is the default type
Map keys and Set members
When decoding a Map to Map | object or a Set to Set, keys and members of type "Simple String" or "Blob String" will be decoded as strings which enables lookups by value, ignoring type mapping. If you want them as Buffers, decode them as Arrays instead.
Not Implemented
These parts of RESP3 are not yet implemented in Redis itself (at the time of writing), so are not yet implemented in the Node-Redis client either:
File: docs/scan-iterators.md
Scan Iterators
:warning: The scan iterators API in v5 has breaking changes from the previous version. For more details, refer to the v4-to-v5 guide.
SCAN results can be looped over using async iterators:
for await (const keys of client.scanIterator()) {
const values = await client.mGet(keys);
}:warning:
keyPrefixgotcha: When the client is configured with akeyPrefix,scanIteratoryields the already-prefixed keys as stored on the server (replies are never un-prefixed). Passing them straight back into a key-prefixing command โ like themGet(keys)above โ prefixes them a second time. For example, withkeyPrefix: 'app:'a yielded'app:foo'becomes'app:app:foo', so themGetsilently reads the wrong keys. Strip the prefix before reusing the keys, or read them with a separate client that has nokeyPrefix. (This affects onlyscanIterator;HSCAN/SSCAN/ZSCANyield fields/members, which are not keys.)
This works with HSCAN, SSCAN, and ZSCAN too:
for await (const entries of client.hScanIterator('hash')) {}
for await (const members of client.sScanIterator('set')) {}
for await (const membersWithScores of client.zScanIterator('sorted-set')) {}You can override the default options by providing a configuration object:
client.scanIterator({
cursor: '0', // optional, defaults to '0'
TYPE: 'string', // `SCAN` only
MATCH: 'patter*',
COUNT: 100
});2. Official Technical Reference & Guides (redis/docs)
Redis Docs
Note: if you are an AI agent, please see AI_AGENT_DEVELOPER_GUIDE.md for more information.
PRs are merged to the main branch of this repo. Merging to main automatically builds and publishes your changes to the live docs site (under /latest) โ there is no separate manual publish step, so allow a little time for the build and CDN cache to refresh after your PR is merged.
Before your PR is merged, you can preview your changes on a per-branch staging URL; the docs team's bot posts the staging links as a comment on your PR.
Site template files and folders
- /archetypes: A Markdown file needs to have some front matter. An archetype defines which front matter is used when using
hugo new content. Right now, the only supported archetype is the default one. Note: We might want to add additional archetypes in the future because most of our pages contain additional meta data properties likelinkTitle. - /content: This folder contains the markdown files. We will have the subfolders like
/develop,/integrate, and/operate - /assets: CSS files, site-wide icons, and images.
- /data: Data files that are accessed by Hugo and rendered with the help of short codes or partials.
- /layouts/partials: HTML templates that are used across sites. Examples are TOCs, breadcrumbs, or headers.
- /layouts/$type: Each page type has at least the following templates to implement
single.htmlandlist.html. Thesingletemplate is used to render a discrete page. Thelisttemplate is used to render a collection of related pages (e.g., all sub-pages). - /layouts/home.html: The home page of the site, that is, the page that is displayed when you open the root path.
- /layouts/404.html: The default 404 page.
- /layouts/shortcodes HTML template snippets that you want to leverage within Markdown files. Examples are tabbed code examples or notification blocks.
- /public: This is the folder that is generated by Hugo. This content needs to be published by site maintainers.
- /static: Any static files that need to be accessed by the site, e.g., CSS or JavaScript.
- /package.json: Node.js dependencies. Tailwind, for example, is installed via the Node package manager (
npm). - /config.toml: Hugo's site configuration, like the root path and menu items. Hugo can access configuration elements when rendering the site. So you can define custom configuration settings here.
- /Makefile: We use make to wrap some Hugo commands and to add additional build steps.
- /tailwind.config.js: This is the Tailwind CSS framwork's configuration file.
- /postcss.config.js: Needed to make Tailwind statically accessible to the site.
Requirements
You will need the following tools to build the site locally:
python3: Python >= 3.8.nodeandnpm: Node.js >= 19.7.0, and the Node.js package manager >= 9.5.0.hugo: Hugo site generator, v0.143.0; the extended edition.make: To run the makefile.git: To manage the source code of this repo.
Build script and data files
The site can be built using the command make all. Here is what's executed:
- Clean the current folder by deleting the generated output of a previous build.
- Install the necessary Node.js and Python dependencies.
- Build the components that were fetched from external Github repositories (e.g., client examples). This fetches source code files and generates data files.
- Execute the
hugocommand to generate the HTML pages. The build result can be found within the folder/public.
The command make serve performs the same steps, but serves the web pages on the local host for development purposes.
The Makefile contains details about the executed commands.
N.B. once you've built the site once, you can continuing working without rebuilding every time you make a change.
Use hugo serve to serve the docs locally.
The only time you really need to rebuild the site locally is when new code samples need to be pulled from the client sites.
Build pipeline
The build pipeline that is defined within .github/workflows/main.yml builds the documentation and uploads it into a GCS bucket:
- Install Hugo
- Check the branch out
- Perform the build as stated before by using
make all - Install the Google Cloud CLI
- Authenticate to the GCS bucket
- Validate the branch name
- Sync the branch with a GCS folder
Hugo specifics
Relative links
We are using the following syntax for Hugo relrefs:
[Link title]({{< relref "link relative to the site's base url" >}})Here is an example:
[Data structure store]({{< relref "/develop/get-started/data-store" >}})It's strongly advised to use relref because it provides the following advantages:
- Links are checked at build time. Any broken references within the site are reported, stopping the build.
- References are prefixed with the site's base URL, which means that they work in builds with a different base URL.
The following needs to be taken into account when using relref: The reference /develop/get-started/data-store and /develop/get-started/data-store/ aren't the same. You must use the trailing slash if the referenced article is an _index.md file within a folder (e.g., .../data-store/ for .../data-store/_index.md). Otherwise, you should not use the trailing slash (e.g., .../get-started/data-store.md).
Images
The image shortcode doesn't need to be closed anymore. Here is an example;
{{< image filename="/images/rc/icon-database-update-status-pending.png" alt="Database update" >}}The filename property value can be any file name path which is relative to the site's base path.
We added a new property class which allows you to override the CSS class of the image. Images have by default a block display in Tailwind. You can change this by setting the class property to inline. The following example shows two images that are in a single line:
{{< image filename="/images/rc/icon-database-update-status-pending.png#no-click" alt="Pending database status" class="inline" >}} {{< image filename="/images/rc/icon-database-status-active.png#no-click" alt="Active database status" class="inline" >}}Templating
Variables are scoped in the context of their block. Overriding a variable within an if block doesn't change the variable value as soon as you leave that block. This is a specific limitation of the Go templating language.
The following will give you the / outside of the condition block if the path was initially set to /:
{{ $path := $parsed.Path }}
{{ if (eq $path "/") }}
{{ $path = "" }}
{{ end }}
{{- $path -}}The solution is to use a scratchpad for storing the 'global' state:
{{ $path := $parsed.Path }}
{{ if (eq $path "/") }}
{{ .Scratch.Set "path" ""}}
{{ else }}
{{ .Scratch.Set "path" $path}}
{{ end }}
{{- .Scratch.Get "path" -}}Child pages
The redis.io template adds a list of links to child pages at the bottom of each section page. You can prevent this by adding the following front matter property:
hideListLinks: trueTailwind specifics
Fonts
The fonts are located in the folder static/fonts. This folder contains typically file of the formats ttf, woff, or woff2. You can perform the following steps to add a font:
Copy your font files to the fonts folder
static/fonts.Ensure that the font is loaded by adding it to the partial
layouts/partials/fonts.html. This partial defines the font using a style sheet and ensures that the font resources are pre-loaded.Here is an example that shows how to define the font face 'Geist Mono' with a regular font weight:
text@font-face { font-family: 'Geist Mono'; src: url('{{ $basePath }}/fonts/GeistMono-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }Make your font available via Tailwind by adding it to the
tailwind.config.jsfile.textmodule.exports = { content: ["content/**/*.md", "layouts/**/*.html", "./tailwindcss.whitelist.txt"], theme: { extend: { fontFamily: { sans: [ 'Space Grotesk', ...defaultTheme.fontFamily.sans ], mono: [ 'Space Mono', 'SF Mono', ...defaultTheme.fontFamily.mono ], monogeist: ['Geist Mono', ...defaultTheme.fontFamily.mono ], }, ...Use a
font-*CSS class within HTML or your style sheets, whereby the*needs to be substituted with one of the Tailwind fonts that you defined in the Tailwind configuration file (tailwind.config.js).Here is an example how to use
font-monogeistin the global CSS fileassets/css/index.cssto change the font of code blocks:text.prose pre > code { @apply bg-none font-monogeist; }
Troubleshooting
NIL pointer error when rendering a section
You might sometimes see the following error message:
nil pointer evaluating page.Page.ParamsThis happened for us when Hugo executed the bannerText logic, which inspected all parent pages by checking if they have a banner text set. The problem was that one of the parent folders was missing an _index.md file, which meant the folder couldn't be identified as a page. The solution was to add an _index.md file to the folder that didn't have it yet.