@keyv/redis
v6.1.0
Published
Redis storage adapter for Keyv
Maintainers
Readme
@keyv/redis
Redis storage adapter for Keyv
Redis storage adapter for Keyv.
Features
- Built on top of @redis/client.
- TTL is handled directly by Redis via absolute
PXATexpiry, with an automatic relativePXfallback for servers older than 6.2 (see Expiration and TTL). - Supports Redis Clusters.
- Url connection string support or pass in your Redis Options
- Easily add in your own Redis client.
- Namespace support for key management.
- Unlink as default delete method for performance.
- Access to the Redis client for advanced use cases.
- Keyv and Redis Libraries are exported for advanced use cases.
createKeyvfunction for easy creation of Keyv instances.- jsDoc comments for easy documentation.
- CJS / ESM and TypeScript supported out of the box.
Table of Contents
- Usage
- Migrating from v5 to v6
- Migrating from v4 to v5
- Using the createKeyv function
- Using the createKeyvNonBlocking function
- Namespaces
- Fixing Double Prefixing of Keys
- Using Generic Types
- Performance Considerations
- Expiration and TTL
- High Memory Usage on Redis Server
- Gracefully Handling Errors and Timeouts
- Using Cacheable with Redis
- Clustering
- Sentinel
- TLS Support
- Redis Cloud
- Keyv Redis Options
- API
- Events
- Migrating from v3 to v4
- About Redis Sets and its Support in v4
- Using with NestJS
- License
Installation
npm install --save keyv @keyv/redisUsage
Here is a standard use case where we implement Keyv and @keyv/redis:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'));
keyv.on('error', handleConnectionError);Here is the same example but with the Keyv instance created with the createKeyv function:
import { createKeyv } from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379');You only have to import the @keyv/redis library if you are using the createKeyv function. 🎉 Otherwise, you can import Keyv and @keyv/redis independently.
Here you can pass in the Redis options directly:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const uri = "redis://localhost:6379";
// NOTE: please use the settings that you need to configure. Check out Keyv Redis Options section
const options = {
namespace: "test",
namespaceSeparator: "->",
clearBatchSize: 100,
useUnlink: true,
noNamespaceAffectsAll: true,
};
const keyvRedis = new KeyvRedis(uri, options);
const keyv = new Keyv(keyvRedis);Or you can create a new Redis instance and pass it in with KeyvOptions such as setting the store:
import Keyv from 'keyv';
import KeyvRedis, { createClient } from '@keyv/redis';
const redis = createClient({ url: 'redis://user:pass@localhost:6379' });
const keyvRedis = new KeyvRedis(redis);
const keyv = new Keyv({ store: keyvRedis});Migrating from v5 to v6
@keyv/redis v6 tracks Keyv's shared version. The adapter already implemented the v6 storage contract (capabilities.expires); these are the redis-specific things to know:
@redis/clientis now v6.createClient,createCluster, andcreateSentinelare still exported from this package.createClienttakes aRedisClientOptionsobject ({ url: 'redis://...' }), not a URI string.- Adapters receive absolute
expires, not relativettl. When you callkeyv.set(key, value, 1000), Keyv converts that millisecond ttl to a Unix-ms deadline and passes it to the adapter. Direct adapter calls should passexpires(Date.now() + ttl), not a relative ttl. - Keyv no longer prefixes keys. Namespacing lives on the adapter (
namespace+namespaceSeparator, default::). You do not needuseKeyPrefix: false— that option was removed from Keyv. keyPrefixSeparatoris nownamespaceSeparator. The option, getter, and setter were renamed to match the other adapters. In plain JavaScript the old option is ignored and the store uses::, so rename it wherever you set it.- Keys written by v5 need matching settings. v5's default setup,
new Keyv(new KeyvRedis(uri)), applied its defaultkeyvnamespace twice, once in Keyv and once in the adapter, sofoowas stored askeyv::keyv:foo. Read those keys withnew Keyv(new KeyvRedis(uri, { namespaceSeparator: '::keyv:' }), { namespace: 'keyv' }). The v5 to v6 migration guide lists the settings for other v5 setups. createKeyvaccepts cluster and sentinel options the same way theKeyvRedisconstructor does.- A failure is reported once. When the adapter rejects because of
throwOnConnectErrororthrowOnErrors, it no longer also emitserror. A failed connection rejects with the connection error as the rejection'scause. See Gracefully Handling Errors and Timeouts.
Migrating from v4 to v5
The major change from v4 to v5 is that we are now using v5 of the @redis/client library which has a new API. This means that some methods have changed but it should be a drop-in replacement for most use cases.
Keyv Redis Options
You can pass in options to the KeyvRedis constructor. Here are the available options:
export type KeyvRedisOptions = {
/**
* Namespace for the current instance.
*/
namespace?: string;
/**
* Separator to use between namespace and key.
*/
namespaceSeparator?: string;
/**
* Number of keys to delete in a single batch.
*/
clearBatchSize?: number;
/**
* Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
*/
useUnlink?: boolean;
/**
* Whether to allow clearing all keys when no namespace is set.
* If set to true and no namespace is set, iterate() will return all keys.
* @default false
*/
noNamespaceAffectsAll?: boolean;
/**
* Throw an error if the client is not connected when trying to connect. By default this is
* `true` so a failed Redis connection throws.
* @default true
*/
throwOnConnectError?: boolean;
/**
* Throw an error if any operation fails. When `false`, failures emit an `error` event
* and return no-op responses (`undefined` for gets, `false` for writes/deletes).
* @default false
*/
throwOnErrors?: boolean;
/**
* Timeout in milliseconds for the full connect handshake (TCP plus Redis HELLO / AUTH).
* When undefined, the Redis client default is used. If set, a connection that does not
* succeed within this time throws, the in-flight attempt is aborted, and leftover sockets
* are not left open. When Keyv constructs the client, this is also passed as
* `socket.connectTimeout` unless that option is already set. Auto-reconnect is disabled
* unless you pass `socket.reconnectStrategy`.
* @default undefined
*/
connectionTimeout?: number;
};You can pass these options when creating a new KeyvRedis instance:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyvRedis = new KeyvRedis('redis://user:pass@localhost:6379', {
namespace: 'my-namespace',
namespaceSeparator: ':',
clearBatchSize: 1000,
useUnlink: true,
noNamespaceAffectsAll: false,
connectionTimeout: 200
});
const keyv = new Keyv({ store: keyvRedis });You can also set these options after the fact by using the KeyvRedis instance properties:
import {createKeyv} from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379');
keyv.store.namespace = 'my-namespace';See API for the full property and method list. Missing keys and swallowed errors return undefined (never null). Events are emitted through Hookified — see Events.
Using the createKeyv function
The createKeyv function is a convenience function that creates a new Keyv instance with the @keyv/redis store. It applies namespace on both Keyv and the adapter so keys are prefixed once. Here is an example of how to use it:
import { createKeyv } from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379');To use a namespace you can do it here and this will set Keyv up correctly to avoid the double namespace issue:
import { createKeyv } from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379', {namespace: 'my-namespace'});Using the createKeyvNonBlocking function
The createKeyvNonBlocking function is a convenience function that creates a new Keyv instance with the @keyv/redis store. It does what createKeyv does but also turns off the adapter's throwOnConnectError and throwOnErrors, the Redis offline queue, and the reconnect strategy, so that when used as a secondary cache in libraries such as cacheable it does not block the primary cache. This is useful when you want to use Redis as a secondary cache and do not want to block the primary cache on connection errors or timeouts when using nonBlocking. Concurrent operations share the in-flight Redis connect so they are not dropped while the client is still opening. Here is an example of how to use it:
import { createKeyvNonBlocking } from '@keyv/redis';
const keyv = createKeyvNonBlocking('redis://user:pass@localhost:6379');
keyv.on('error', (error) => console.error('Redis error', error));As with any Keyv instance, attach an error listener so a failed operation returns a fallback value instead of rejecting. See Gracefully Handling Errors and Timeouts.
With a URI or client options, createKeyvNonBlocking creates the client with the offline queue and reconnect already off. When Redis is down, an operation fails as soon as its connection attempt does and returns its fallback value, instead of waiting for Redis to come back. A refused connection fails immediately. A host that doesn't respond waits for the socket's connectTimeout (5 seconds by default), which the connectionTimeout option can shorten.
node-redis fixes a client's reconnect strategy when the client is created, so a client you pass in keeps its own. Create it with socket: { reconnectStrategy: false } to get the same behavior. Its offline queue is still turned off:
import { createClient, createKeyvNonBlocking } from '@keyv/redis';
const client = createClient({ url: 'redis://localhost:6379', socket: { reconnectStrategy: false } });
const keyv = createKeyvNonBlocking(client);Cluster and sentinel connections keep their own offline queue and reconnect settings. Only the throw settings change.
Namespaces
By default namespacing is turned off, this is done because it causes much more memory / performance usage for Redis.
Redis does not treat colons (:) or namespaces as special—there are no hierarchical keys or namespace mechanics internally. The dramatic memory savings you see when removing prefixes like namespace: come from one thing: key length. Redis stores every key as a full string in memory, wrapped in an SDS structure and allocated by jemalloc. Longer keys (e.g., namespace:key123) fall into larger jemalloc size classes, require more bytes for the SDS header, and cause more fragmentation. Shorter keys (e.g., key123) fit into much smaller slabs, pack tightly, and result in far more stable and predictable memory usage.
This means the colon is not the issue—the extra characters are. A key that goes from ~18 bytes to ~6 bytes can use half the memory per key once overhead, allocator classes, and fragmentation are considered. Multiply that by hundreds of thousands or millions of keys, and memory usage becomes significantly smaller and much more stable simply because the keys are shorter.
How to use Namespaces
You can set a namespace for your keys. This is useful if you want to manage your keys in a more organized way. Here is an example of how to set a namespace with the store option:
import Keyv from 'keyv';
import KeyvRedis, { createClient } from '@keyv/redis';
const redis = createClient({ url: 'redis://user:pass@localhost:6379' });
const keyvRedis = new KeyvRedis(redis);
const keyv = new Keyv({ store: keyvRedis, namespace: 'my-namespace' });To make this easier, you can use the createKeyv function which will automatically set the namespace option to the KeyvRedis instance:
import { createKeyv } from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379', { namespace: 'my-namespace' });This will prefix all keys with my-namespace:: (the default namespaceSeparator is ::). You can also set the namespace after the fact:
keyv.namespace = 'my-namespace';clear() and iterator() only touch keys that start with the namespace and separator (my-namespace::). Both are matched literally, so *, ?, [, ] and \ in them have no special meaning to SCAN: clearing tenant* leaves tenant-prod alone. Clearing my-namespace does also clear a namespace that extends it with the separator, such as my-namespace::archive, because a key can contain the separator too. Keep the separator out of namespace names if you need them kept apart.
NOTE: If you plan to do many clears or deletes, it is recommended to read the Performance Considerations section.
Fixing Double Prefixing of Keys
In v6, Keyv does not prefix keys. The Redis adapter owns namespacing, so this:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'), { namespace: 'my-namespace' });stores keys as my-namespace::key. createKeyv does the same wiring for you:
import { createKeyv } from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379', { namespace: 'my-namespace' });If you are upgrading from v5, drop useKeyPrefix — it no longer exists. Do not also set Redis client's keyPrefix; use the adapter namespace instead so SCAN/clear/iterator stay in sync.
Using Generic Types
When initializing KeyvRedis, you can specify the type of the values you are storing and you can also specify types when calling methods:
import Keyv from 'keyv';
import KeyvRedis, { createClient } from '@keyv/redis';
type User = {
id: number
name: string
}
const redis = createClient({ url: 'redis://user:pass@localhost:6379' });
const keyvRedis = new KeyvRedis<User>(redis);
const keyv = new Keyv({ store: keyvRedis });
await keyv.set("user:1", { id: 1, name: "Alice" })
const user = await keyv.get("user:1")
console.log(user.name) // 'Alice'
// specify types when calling methods
const user = await keyv.get<User>("user:1")
console.log(user.name) // 'Alice'Performance Considerations
With namespaces being prefix based it is critical to understand some of the performance considerations we have made:
clear()- We use theSCANcommand to iterate over keys. This is a non-blocking command that is more efficient thanKEYS. In addition we are usingUNLINKby default instead ofDEL. Even with that if you are iterating over a large dataset it can still be slow. It is highly recommended to use thenamespaceoption to limit the keys that are being cleared and if possible to not use theclear()method in high performance environments. If you don't set namespaces, you can enablenoNamespaceAffectsAllto clear all keys using theFLUSHDBcommand which is faster and can be used in production environments.delete()- By default we are now usingUNLINKinstead ofDELfor deleting keys. This is a non-blocking command that is more efficient thanDEL. If you are deleting a large number of keys it is recommended to use thedeleteMany()method instead ofdelete().clearBatchSize- TheclearBatchSizeoption is set to1000by default. This is because Redis has a limit of 1000 keys that can be deleted in a single batch. If no namespace is defined and noNamespaceAffectsAll is set totruethis option will be ignored and theFLUSHDBcommand will be used instead.useUnlink- This option is set totrueby default. This is becauseUNLINKis a non-blocking command that is more efficient thanDEL. If you are not usingUNLINKand are doing a lot of deletes it is recommended to set this option totrue.setMany,getMany,deleteMany- These methods are more efficient than their singular counterparts. These will be used by default in theKeyvlibrary such as when usingkeyv.delete(string[])it will usedeleteMany().
If you want to see even better performance please see the Using Cacheable with Redis section as it has non-blocking and in-memory primary caching that goes along well with this library and Keyv.
Expiration and TTL
Keyv hands this adapter an absolute expiry — a Unix timestamp in milliseconds — computed once on the Keyv host. The adapter writes it to Redis with SET ... PXAT, the absolute-expiry option. Because the deadline is absolute, it is immune to clock skew and to any latency between Keyv computing the expiry and Redis receiving the command: a key set to expire at T expires at T no matter how long the write takes to arrive.
PXAT was added in Redis 6.2. For older servers the adapter automatically falls back to the relative PX option (the remaining lifetime in milliseconds):
- On the first expiring write, the adapter runs
INFO serveronce, parsesredis_version, and caches whether the server is 6.2+. Every later write reuses that cached answer, so detection costs oneINFOround-trip per connection, not per write. - 6.2 or newer → the write uses
PXAT: expires(absolute). - Older than 6.2 → the write uses
PX: max(1, expires - Date.now())(relative, computed at write time). An expiry already in the past becomesPX: 1because Redis rejectsSET ... PX 0. - If the version cannot be determined — for example a cluster where
INFOisn't directly available, or a transient error reading it — the adapter assumesPXATis supported, since such deployments are overwhelmingly modern. The cached result means it won't keep retryingINFOon every write.
The same detection and fallback apply to setMany, including the per-hash-slot grouping used in cluster mode. No configuration is required; you always call keyv.set(key, value, ttl) with a relative millisecond ttl (or rely on the ttl option) and the adapter chooses the correct Redis option for your server.
The relative PX fallback re-derives the remaining lifetime store-side, so on pre-6.2 servers a large write-to-Redis latency could in theory shorten the effective TTL by that latency. On 6.2+ (PXAT) there is no such window. If you need the skew-immune absolute behaviour, run Redis 6.2 or later.
High Memory Usage on Redis Server
This is because we are using UNLINK by default instead of DEL. This is a non-blocking command that is more efficient than DEL but will slowly remove the memory allocation.
If you are deleting or clearing a large number of keys you can disable this by setting the useUnlink option to false. This will use DEL instead of UNLINK and should reduce the memory usage.
const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379', { useUnlink: false }));
// Or
keyv.store.useUnlink = false;Gracefully Handling Errors and Timeouts
Attach an error listener to the Keyv instance when you use @keyv/redis. Keyv handles errors the way a Node.js EventEmitter does: when an operation fails, it emits error, and then
- with a listener attached, the operation returns a fallback value, such as
undefinedfromgetorfalsefromset; - with no listener attached, the operation rejects with the error.
The Redis client also reports connection problems as error events outside any Keyv call, for example when a connection drops. Keyv forwards these to its own error event. With no listener attached, such an error is thrown with no call to catch it, which can crash your process.
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'));
keyv.on('error', (error) => {
console.error('error', error);
});See Error Handling in the Keyv README for the fallback value each method returns.
The adapter has its own options that decide whether KeyvRedis rejects or reports a failure as an error event:
throwOnConnectError(defaulttrue): reject when the connection fails, with the connection error as the rejection'scause. When it isfalse, the adapter emits the connection error as anerrorevent instead.throwOnErrors(defaultfalse): reject when an operation fails. When it isfalse, the adapter emitserrorand returns a no-op value onget,getMany,set,setMany,delete, anddeleteManyif the connection is lost.
The adapter reports each failure once: when it rejects, it does not also emit error. Used through Keyv, a rejection from the adapter is handled by the rule above: Keyv emits error, then returns a fallback value if a listener is attached or rejects if none is.
There is a default Reconnect Strategy if you pass in just a uri connection string we will automatically create a Redis client for you with the following reconnect strategy:
export const defaultReconnectStrategy = (attempts: number): number | Error => {
// Exponential backoff base: double each time, capped at 2s.
// Parentheses make it clear we do (2 ** attempts) first, then * 100
const backoff = Math.min((2 ** attempts) * 100, 2000);
// Add random jitter of up to ±50ms to avoid thundering herds:
const jitter = (Math.random() - 0.5) * 100;
return backoff + jitter;
};Using Cacheable with Redis
If you are wanting to see even better performance with Redis, you can use Cacheable which is a multi-layered cache library that has in-memory primary caching and non-blocking secondary caching. Here is an example of how to use it with Redis:
import KeyvRedis from '@keyv/redis';
import Cacheable from 'cacheable';
const secondary = new KeyvRedis('redis://user:pass@localhost:6379');
const cache = new Cacheable( { secondary } );For even higher performance you can set the nonBlocking option to true:
const cache = new Cacheable( { secondary, nonBlocking: true } );This will make it so that the secondary does not block the primary cache and will be very fast. 🚀
Note:
cacheable2 depends on Keyv v5 and wraps a store like this one in its own v5Keyv. A v5Keyvpasses a relativettlwhere a v6 adapter expects an absoluteexpires, so entries expire at the wrong time, usually at once. Check which Keyv version yourcacheableneeds withnpm view cacheable dependencies.keyv, and keep this setup on v5 until it supports v6. See Libraries That Embed Keyv v5.
Clustering
If you are using a Redis Cluster, you can pass in the redisOptions directly. Here is an example of how to do that:
import Keyv from 'keyv';
import KeyvRedis, { createCluster } from '@keyv/redis';
const cluster = createCluster({
rootNodes: [
{
url: 'redis://127.0.0.1:7000',
},
{
url: 'redis://127.0.0.1:7001',
},
{
url: 'redis://127.0.0.1:7002',
},
],
});
const keyv = new Keyv({ store: new KeyvRedis(cluster) });You can learn more about the createCluster function in the documentation at https://github.com/redis/node-redis/tree/master/docs.
Sentinel
If you are using Sentinel to provide high availability for your Redis instances, you can pass in the sentinel options directly to the KeyvRedis constructor:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv({
store: new KeyvRedis({
name: 'mymaster',
sentinelRootNodes: [
{ host: '127.0.0.1', port: 26379 },
{ host: '127.0.0.1', port: 26380 },
{ host: '127.0.0.1', port: 26381 },
],
}),
});For TypeScript users, the RedisSentinelOptions type is exported from the package:
import KeyvRedis, { type RedisSentinelOptions } from '@keyv/redis';
const sentinelOptions: RedisSentinelOptions = {
name: 'mymaster',
sentinelRootNodes: [
{ host: '127.0.0.1', port: 26379 },
{ host: '127.0.0.1', port: 26380 },
{ host: '127.0.0.1', port: 26381 },
],
};
const keyvRedis = new KeyvRedis(sentinelOptions);You can learn more about Sentinel configuration in the documentation at https://github.com/redis/node-redis/tree/master/docs.
TLS Support
Here is an example of how to use TLS using the redisOptions:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const tlsOptions = {
socket: {
host: 'localhost',
port: 6379,
tls: true, // Enable TLS connection
rejectUnauthorized: false, // Ignore self-signed certificate errors (for testing)
// Alternatively, provide CA, key, and cert for mutual authentication
ca: fs.readFileSync('/path/to/ca-cert.pem'),
cert: fs.readFileSync('/path/to/client-cert.pem'), // Optional for client auth
key: fs.readFileSync('/path/to/client-key.pem'), // Optional for client auth
}
};
const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });Redis Cloud
You connect to Redis Cloud the same way you connect to any Redis server, with a URI or with Redis client options. KeyvRedis creates a client from a URI string. It passes an options object, unchanged except for connectionTimeout when you set it, to createClient from @redis/client. That means url, username, password, and every socket TLS option work exactly as documented for @redis/client. createKeyv accepts the same arguments.
Endpoint and credentials
The database endpoint (host and port) is on the database's Configuration tab in the Redis Cloud console: in the Access section for Essentials databases, or the General section for Pro databases. By default the database has a user named default. To see its password, use Default user > Configure for an Essentials database, or the Security section of the Configuration tab for a Pro database. If you turned on role-based access control and turned off the default user, use the username and password of a data access role instead.
With TLS turned on, the URI uses the rediss:// scheme (two s's). Without redis_ca.pem, the URI below fails to connect when the database's certificate was issued by a Redis Cloud CA, so trust that CA as shown in Trusting redis_ca.pem.
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv(new KeyvRedis('rediss://default:password@redis-host:12345'));
keyv.on('error', (error) => console.error('Redis error', error));Replace password, redis-host, and 12345 with your values. Encode a password that contains reserved URI characters, such as @, :, /, or #, with encodeURIComponent. A redis:// URI connects without TLS, which a TLS-enabled database refuses.
Turning on TLS
TLS is off by default. Turn it on from the Security section of the database's Configuration tab. It is available on paid Essentials and Pro plans, but not on the Free plan. Redis Cloud recommends TLS for any database reached over its public endpoint. Once TLS is on, every connection must use it.
Trusting redis_ca.pem
Download the Redis Cloud CA bundle, redis_ca.pem, with Download server certificate on the database's Configuration tab, or from Account Settings > Security. The file holds several certificates, including Redis Cloud's own root CAs, which Node.js does not trust by default. Without the bundle, a rediss:// URI checks the certificate only against Node.js's built-in CAs, which fails for databases whose certificates were issued by a Redis Cloud CA. Pass the whole file as socket.ca. Node.js loads every certificate in it.
import fs from 'node:fs';
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv(new KeyvRedis({
url: 'rediss://default:password@redis-host:12345',
socket: {
tls: true,
ca: fs.readFileSync('/path/to/redis_ca.pem'),
},
}));
keyv.on('error', (error) => console.error('Redis error', error));You can set the host, port, and credentials as separate options instead of a url:
const keyv = new Keyv(new KeyvRedis({
username: 'default',
password: 'password',
socket: {
host: 'redis-host',
port: 12345,
tls: true,
ca: fs.readFileSync('/path/to/redis_ca.pem'),
},
}));Keep these points in mind:
- Leave
rejectUnauthorizedunset so Node.js verifies the certificate. The TLS Support sample sets it tofalse, which is only suitable for a local server with a self-signed certificate. - Connect with the endpoint hostname from the console, not an IP address. The certificate check compares hostnames.
socket.tlsmust match the URI scheme, so it must betruewithrediss://. Otherwise@redis/clientthrows.- A URI string gets the adapter's
defaultReconnectStrategy. With an options object,@redis/clientuses its own reconnect behavior unless you setsocket.reconnectStrategy, for example todefaultReconnectStrategy, which you can import from@keyv/redis.
Mutual TLS
If the database requires client authentication (Mutual TLS in the TLS settings), add the client certificate and private key you registered there to the same socket options:
const keyv = new Keyv(new KeyvRedis({
username: 'default',
password: 'password',
socket: {
host: 'redis-host',
port: 12345,
tls: true,
ca: fs.readFileSync('/path/to/redis_ca.pem'),
cert: fs.readFileSync('/path/to/client.crt'),
key: fs.readFileSync('/path/to/client.key'),
},
}));Other hosted Redis services use the same pattern: a rediss:// URI or socket.tls: true, plus the CA certificate the provider gives you.
API
KeyvRedis extends Hookified (on, once, emit, hooks) and implements the Keyv storage adapter contract. Missing keys and swallowed errors return undefined (never null).
Properties
- capabilities - Adapter capability descriptor.
capabilities.expiresistrue(absolute Unix-msexpiresonset/setMany). - client - The Redis client, cluster, or sentinel connection. Assigning a new connection re-wires Hookified event listeners and resets PXAT detection. Type:
RedisClientConnectionType. - namespace - Namespace used to prefix keys.
undefinedmeans no prefixing. Default:undefined. - namespaceSeparator - Separator between namespace and key. May be
"". Default:"::". - clearBatchSize -
SCAN/ delete batch size. Must be greater than0or anerrorevent is emitted. Default:1000. - useUnlink - Use
UNLINKinstead ofDEL. Default:true. - noNamespaceAffectsAll - When no namespace is set,
clear()/iterator()affect all keys (including namespaced ones). Default:false. - throwOnConnectError - Throw when connect fails, with the connection error as the
cause, instead of emittingerror. Default:true. - throwOnErrors - Throw on operation failures instead of emitting
errorand returning a no-op. Default:false. - connectionTimeout - Timeout in milliseconds for the full connect handshake (TCP plus Redis HELLO / AUTH).
undefineduses the Redis client default. On expiry the in-flight connect is aborted so sockets and timers are not left active. When Keyv constructs the client, this is also passed assocket.connectTimeoutunless that option is already set. Auto-reconnect is disabled unless you passsocket.reconnectStrategy.
Methods
- constructor([connect], [options]) -
connectis a URI string, client/cluster/sentinel options (KeyvRedisConnect), or an existing connection. See Keyv Redis Options. - getClient() - Return the ready client, connecting first if needed. Concurrent callers share the in-flight connect and wait until
isReady(not onlyisOpen). OnconnectionTimeout, the hung handshake is aborted so sockets are not left open. ReturnsPromise<RedisClientConnectionType>. - set(key, value, [expires]) - Set a key.
expiresis an absolute Unix timestamp in milliseconds. ReturnsPromise<boolean>. Through Keyv, pass a relative millisecondttltokeyv.set— Keyv converts it toexpires. - setMany(entries) - Set
KeyvStorageEntryobjects ({ key, value, expires? }) viaMULTI/EXEC. ReturnsPromise<boolean[]>(per-entry success). Cluster mode groups by hash slot. - get(key) - Get a key. Returns the value or
undefinedif missing (Redisnullis mapped toundefined). - getMany(keys) - Get multiple keys. Each missing entry is
undefined. - has(key) - Returns
Promise<boolean>. - hasMany(keys) - Returns
Promise<boolean[]>. - delete(key) - Returns
Promise<boolean>. - deleteMany(keys) - Returns
Promise<boolean[]>. - clear() - Clear keys in the namespace. With no namespace, clears un-prefixed keys unless
noNamespaceAffectsAllistrue(FLUSHDB). - iterator() - Async generator of
[key, value]pairs. Uses the instance namespace. Missing values areundefined. - disconnect([force]) - Graceful
close()(QUIT) whenforceis false;destroy()whentrue. - createKeyPrefix(key, [namespace]) - Returns
namespace::key(orkeywhen namespace is omitted). - getKeyWithoutPrefix(key, [namespace]) - Strips a leading namespace prefix.
- isCluster() -
trueif the connection is a Redis cluster. - isSentinel() -
trueif the connection is a Redis sentinel. - getMasterNodes() - Cluster master node clients, or
[client]when standalone.
Helpers
- createKeyv([connect], [options]) - Keyv instance with this adapter. Applies
namespaceon both Keyv and the store.connectaccepts the sameKeyvRedisConnecttypes as the constructor (including cluster/sentinel). Defaults to"redis://localhost:6379"whenconnectis omitted. - createKeyvNonBlocking([connect], [options]) - Same as
createKeyv, then disables throws, the offline queue, and reconnect for secondary-cache use. A client passed in keeps its own reconnect strategy, and cluster and sentinel connections only have their throw settings changed. See Using thecreateKeyvNonBlockingfunction. Concurrent operations wait for the shared in-flight connect instead of being dropped. - defaultReconnectStrategy(attempts) - Default socket reconnect delay when a URI string is passed to the constructor. Exponential backoff capped at 2s, plus up to ±50ms of jitter. Returns a delay in milliseconds.
Events
KeyvRedis extends Hookified and re-emits these events from the Redis client onto the adapter:
| Event | Payload | When |
| --- | --- | --- |
| error | Error (or a string for invalid clearBatchSize) | Client errors, and connect or operation failures that the adapter does not reject (see throwOnConnectError and throwOnErrors) |
| connect | the Redis connection | The client connects |
| disconnect | the Redis connection | The client disconnects |
| reconnecting | reconnect info from @redis/client | The client is reconnecting |
import {createKeyv} from '@keyv/redis';
const keyv = createKeyv('redis://user:pass@localhost:6379');
const store = keyv.store;
store.on('error', (error) => {
console.error('adapter error', error);
});
store.on('connect', () => {
console.log('Redis connected');
});
store.once('disconnect', () => {
console.log('Redis disconnected');
});
store.on('reconnecting', () => {
console.log('Redis reconnecting');
});error, connect, disconnect, and reconnecting are re-emitted on the adapter. Other Redis-client-only events (for example ready) can still be listened on store.client:
store.client.on('ready', () => {
console.log('Redis client ready');
});Client events: https://www.npmjs.com/package/redis#events
Migrating from v3 to v4
Note: This section applies to versions before v6. Keyv v6 removed the
useKeyPrefixoption: TypeScript rejects it and JavaScript ignores it. In v6 the adapter owns namespacing, so leave the option out. See Fixing Double Prefixing of Keys.
Overall the API is the same as v3 with additional options and performance improvements. Here are the main changes:
- The
ioredislibrary has been removed in favor of theredisakanode-redislibrary. If you want to use ioredis you can use@keyv/valkey - The
useUnlinkoption has been added to useUNLINKinstead ofDELand set to true by default. - The
clearBatchSizeoption has been added to set the number of keys to delete in a single batch. - The
clear()anddelete()methods now useUNLINKinstead ofDEL. If you want to useDELyou can set theuseUnlinkoption tofalse. - BREAKING: We no longer support redis sets. This is due to the fact that it caused significant performance issues and was not a good fit for the library.
- BREAKING: YOUR PREVIOUS KEYS WILL NOT BE VALID. This is because of the fixe of the namespace support and how it is handled. Now, when using
keyvwith@keyv/redisas the storage adapter you can do the following:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const redis = new KeyvRedis('redis://user:pass@localhost:6379');
const keyv = new Keyv({ store: redis, namespace: 'my-namespace', useKeyPrefix: false });This will make it so the storage adapter @keyv/redis will handle the namespace and not the keyv instance. If you leave it on it will just look duplicated like my-namespace:my-namespace:key.
About Redis Sets and its Support in v4
We no longer support redis sets. This is due to the fact that it caused significant performance issues and was not a good fit for the library.
Using with NestJS
You can integrate
@keyv/rediswith NestJS by creating a customCacheModule. This allows you to use Keyv as a cache store in your application.
Note:
cache-manager7 depends on Keyv v5. It reads expiry data withstore.get(key, { raw: true }), which a v6Keyvignores, sottl()and the refresh logic inwrap()stop working with a v6 store. Check which Keyv version yourcache-managerneeds withnpm view cache-manager dependencies.keyv. See Libraries That Embed Keyv v5.
1. Install Dependencies
npm install @keyv/redis keyv @nestjs/cache-manager cache-manager cacheable2. Create a Cache Module
Create a file cache.module.ts:
import { Module } from '@nestjs/common';
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
import { createKeyv } from '@keyv/redis';
@Module({
imports: [
NestCacheModule.registerAsync({
useFactory: () => ({
stores: [createKeyv('redis://localhost:6379')],
}),
}),
],
providers: [],
exports: [],
})
export class CacheModule {}3. Import the Cache Module in AppModule
Update app.module.ts:
import { Module } from '@nestjs/common';
import { CacheModule } from './modules/config/cache/cache.module';
@Module({
imports: [
CacheModule, // Import your custom cache module
// other modules...
],
})
export class AppModule {}4. Create the Cache Service
Create a file cache.service.ts:
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async get<T>(key: string): Promise<T | undefined> {
return this.cacheManager.get<T>(key);
}
async set<T>(key: string, value: T, ttl?: number): Promise<boolean> {
await this.cacheManager.set(key, value, ttl);
return true;
}
async delete(key: string): Promise<void> {
await this.cacheManager.del(key);
}
}5. Register CacheService in CacheModule
Update cache.module.ts:
import { Module } from '@nestjs/common';
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
import { createKeyv } from '@keyv/redis';
import { CacheService } from './services/cache.service';
@Module({
imports: [
NestCacheModule.registerAsync({
useFactory: () => ({
stores: [createKeyv('redis://localhost:6379')],
}),
}),
],
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}6. Import CacheModule in the Target Module (e.g. TaskModule)
import { Module } from '@nestjs/common';
import { TaskService } from './task.service';
import { TaskRepository } from './repositories/task.repository';
import { CacheModule } from 'src/modules/config/cache/cache.module';
@Module({
imports: [CacheModule],
providers: [TaskService, TaskRepository],
})
export class TaskModule {}7. Using the Cache in a Service
import { Injectable, NotFoundException } from '@nestjs/common';
import { TaskRepository } from '../repositories/task.repository';
import { TaskDto } from '../dto/task.dto';
import { CacheService } from 'src/modules/config/cache/services/cache.service';
@Injectable()
export class TaskService {
constructor(
private readonly taskRepository: TaskRepository,
private readonly cache: CacheService, // Inject the CacheService
) {}
async findById(id: number): Promise<TaskDto> {
const cacheKey = `task:${id}`;
// 1. Try to get from cache
const cached = await this.cache.get<TaskDto>(cacheKey);
if (cached) {
return cached;
}
// 2. If not found in cache, fetch from database
const task = await this.taskRepository.findById(id);
if (!task) {
throw new NotFoundException('Task not found');
}
// 3. Set in cache for future requests
await this.cache.set(cacheKey, task, 300 * 1000); // 5 minutes TTL
return task;
}
}You can learn more about caching in NestJS in the official documentation.
