node-redis Documentation

TypeScript nodejs & redis-client repository by @redis. AI agent rules & system prompts with 18k stars.

17,568 stars TypeScript TypeScript 1 file ยท ~17,362 tokens #node-redis#nodejs#redis#redis-client#redis-cluster
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy node-redis on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name node-redis --restart always redis/node-redis:latest

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

Try the Redis Cloud

Dive in developer tutorials

Join the Redis community

Work at Redis

Installation

Start a redis via docker:

bash
docker run -p 6379:6379 -d redis:8.0-rc1

To install node-redis, simply:

bash
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

typescript
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):

js
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]:

typescript
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.):

typescript
// 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:

typescript
await client.set("key", "value", {
  EX: 10,
  NX: true,
});

Replies will be transformed into useful data structures:

typescript
await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' }
await client.hVals("key"); // ['value1', 'value2']

Buffers are supported as well:

typescript
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:

typescript
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():

typescript
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:

typescript
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:

javascript
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:

javascript
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:

typescript
for await (const keys of client.scanIterator()) {
  console.log(keys, await client.mGet(keys));
}

This works with HSCAN, SSCAN, and ZSCAN too:

typescript
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:

typescript
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

typescript
// 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/xxhash peer 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.

bash
npm install @node-rs/xxhash
typescript
import { 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 to
client.destroy().

typescript
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.

typescript
// 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".

typescript
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().

typescript
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

typescript
import { createClient, OpenTelemetry } from "redis";

OpenTelemetry.init({
  metrics: {
    enabled: true
  }
});

const client = createClient()

await client.connect();
// ... use the client as usual

Important: 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 error events. If a client doesn't have at least one error listener registered and
an error occurs, that error will be thrown and the Node.js process will exit. See the > EventEmitter docs 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:

See Supported Redis Versions.

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:

  1. false -> do not reconnect, close the client and flush the command queue.
  2. number -> wait for X milliseconds before reconnecting.
  3. (retries: number, cause: Error) => false | number | Error -> number is the same as configuring a number directly, Error is the same as false, but with a custom error.

By default the strategy uses exponential backoff, but it can be overwritten like so:

javascript
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

javascript
createClient({
  socket: {
    tls: true,
    ca: '...',
    cert: '...'
  }
});

Create a SSL client using a self-signed certificate

javascript
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.

javascript
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, and RANDOMKEY return keys that still include the prefix.
  • Because returned keys keep the prefix, scanIterator yields already-prefixed keys. Feeding
    them straight back into a key-prefixing command double-prefixes them โ€” e.g. with keyPrefix: 'app:',
    a yielded 'app:foo' passed to client.mGet(...) becomes 'app:app:foo'. Strip the prefix first,
    or use a client without a keyPrefix. See Scan Iterators.
  • SCAN/KEYS/HSCAN/โ€ฆ MATCH patterns 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/transformArguments helper does not apply keyPrefix.

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:

javascript
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 rootNodes and defaults configuration 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, or undefined to 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.
javascript
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.

javascript
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:

javascript
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.

javascript
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 error events. If a cluster doesn't have at least one error listener registered and
an error occurs, that error will be thrown and the Node.js process will exit. See the > EventEmitter docs 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:

javascript
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 case

All 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:

javascript
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:

javascript
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:

javascript
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:

javascript
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.

javascript
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.

javascript
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:

javascript
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:

javascript
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.

typescript
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 BLPOP or BLMOVE.
  • You're using transactions and need to WATCH a 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:

javascript
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):

javascript
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.

javascript
await pool.sendCommand(['PING']); // 'PONG'
await client.ping(); // 'PONG'
await client.withTypeMapping({
  [RESP_TYPES.SIMPLE_STRING]: Buffer
}).ping(); // Buffer

Transactions

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:

javascript
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
#!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.

text
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.

typescript
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.

typescript
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'); // 3

File: 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:

javascript
const subscriber = client.duplicate();
subscriber.on('error', err => console.error(err));
await subscriber.connect();

When working with either RedisCluster or RedisSentinel, 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:

typescript
(
  channel: string,
  listeners: {
    buffers: Set<Listener>;
    strings: Set<Listener>;
  }
)

When working with RedisCluster, this is handled automatically for you.

Subscribing

javascript
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

javascript
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.

javascript
await client.unsubscribe();
await client.pUnsubscribe();
// Use sUnsubscribe for sharded Pub/Sub:
await client.sUnsubscribe();

To unsubscribe from specific channels:

javascript
await client.unsubscribe('channel');
await client.unsubscribe(['1', '2']);

To unsubscribe a specific listener:

javascript
await client.unsubscribe('channel', listener);

Buffers

Publishing and subscribing using Buffers is also supported:

javascript
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:

javascript
for await (const keys of client.scanIterator()) {
  const values = await client.mGet(keys);
}

:warning: keyPrefix gotcha: When the client is configured with a keyPrefix, scanIterator yields the already-prefixed keys as stored on the server (replies are never un-prefixed). Passing them straight back into a key-prefixing command โ€” like the mGet(keys) above โ€” prefixes them a second time. For example, with keyPrefix: 'app:' a yielded 'app:foo' becomes 'app:app:foo', so the mGet silently reads the wrong keys. Strip the prefix before reusing the keys, or read them with a separate client that has no keyPrefix. (This affects only scanIterator; HSCAN/SSCAN/ZSCAN yield fields/members, which are not keys.)

This works with HSCAN, SSCAN, and ZSCAN too:

javascript
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:

javascript
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 like linkTitle.
  • /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.html and list.html. The single template is used to render a discrete page. The list template 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.
  • node and npm: 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:

  1. Clean the current folder by deleting the generated output of a previous build.
  2. Install the necessary Node.js and Python dependencies.
  3. Build the components that were fetched from external Github repositories (e.g., client examples). This fetches source code files and generates data files.
  4. Execute the hugo command 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:

  1. Install Hugo
  2. Check the branch out
  3. Perform the build as stated before by using make all
  4. Install the Google Cloud CLI
  5. Authenticate to the GCS bucket
  6. Validate the branch name
  7. Sync the branch with a GCS folder

Hugo specifics

We are using the following syntax for Hugo relrefs:

text
[Link title]({{< relref "link relative to the site's base url" >}})

Here is an example:

text
[Data structure store]({{< relref "/develop/get-started/data-store" >}})

It's strongly advised to use relref because it provides the following advantages:

  1. Links are checked at build time. Any broken references within the site are reported, stopping the build.
  2. 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;

text
{{< 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:

text
{{< image filename="/images/rc/icon-database-update-status-pending.png#no-click" alt="Pending database status" class="inline" >}} &nbsp; {{< 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 /:

text
{{ $path := $parsed.Path }}
{{ if (eq $path "/") }}
	{{ $path = "" }}
{{ end }}

{{- $path -}}

The solution is to use a scratchpad for storing the 'global' state:

text
{{ $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:

text
hideListLinks: true

Tailwind 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.js file.

    text
    module.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-monogeist in the global CSS file assets/css/index.css to 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:

text
nil pointer evaluating page.Page.Params

This 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.