@keyv/valkey

Valkey storage adapter for Keyv

build codecov npm npm

Valkey storage adapter for Keyv.

Valkey is the open source replacement to Redis which decided to do a dual license approach moving forward. Valkey is a drop-in replacement for Redis and is fully compatible with the Redis protocol.

We are using the iovalkey which is a Node.js client for Valkey based on the ioredis client.

Table of Contents

Install

npm install --save keyv @keyv/valkey

Usage

This is using the helper createKeyv function to create a Keyv instance with the Valkey storage adapter:

import {createKeyv} from '@keyv/valkey';

const keyv = createKeyv('redis://localhost:6379');
keyv.on('error', handleConnectionError);
await keyv.set('foo', 'bar');
console.log(await keyv.get('foo')); // 'bar'

If you want to specify the KeyvValkey class directly, you can do so:

import Keyv from 'keyv';
import KeyvValkey from '@keyv/valkey';

const keyv = new Keyv(new KeyvValkey('redis://user:pass@localhost:6379', { disable_resubscribing: true }));

Or you can manually create a storage adapter instance and pass it to Keyv:

import Keyv from 'keyv';
import KeyvValkey from '@keyv/valkey';

const keyvValkey = new KeyvValkey('redis://user:pass@localhost:6379');
const keyv = new Keyv({ store: keyvValkey });

Or reuse a previous Redis instance:

import Keyv from 'keyv';
import Redis from 'iovalkey';
import KeyvValkey from '@keyv/valkey';

const redis = new Redis('redis://user:pass@localhost:6379');
const keyvValkey = new KeyvValkey(redis);
const keyv = new Keyv({ store: keyvValkey });

Or reuse a previous Redis cluster:

import Keyv from 'keyv';
import Redis from 'iovalkey';
import KeyvValkey from '@keyv/valkey';

const cluster = new Redis.Cluster([{ host: '127.0.0.1', port: 7001 }]);
const keyvValkey = new KeyvValkey(cluster);
const keyv = new Keyv({ store: keyvValkey });

Using the createKeyv function

createKeyv is a convenience factory that returns a Keyv instance with the Valkey adapter already wired. The namespace is taken from the adapter so it is preserved whether it was passed in the connect options object or the second argument.

import {createKeyv} from '@keyv/valkey';

const keyv = createKeyv('redis://localhost:6379', { namespace: 'my-app' });
console.log(keyv.namespace); // 'my-app'
console.log(keyv.store.namespace); // 'my-app'

If no connect argument is provided, the default URI is redis://localhost:6379.

Migrating to v6

Breaking changes

Properties instead of opts

In v6, all configuration options are exposed as top-level properties with getters and setters:

// v6
store.useSets; // false (default)
store.useSets = true;
store.namespace = 'my-namespace';

useRedisSets renamed to useSets

The useRedisSets option has been renamed to useSets. The useRedisSets property is still available as a deprecated getter/setter on the class but will be removed in a future version.

redis property renamed to client

The redis property has been renamed to client and is now properly typed as Redis | Cluster instead of any. Update any code that accesses the underlying iovalkey instance:

// v5
store.redis;

// v6
store.client;

useSets default changed from true to false

The default value of useSets has changed from true to false for performance reasons. When enabled, a set is maintained for each namespace to track keys, which can lead to memory leaks in high-throughput scenarios. If you depend on the previous behavior, explicitly set useSets: true in your options:

const store = new KeyvValkey('redis://localhost:6379', { useSets: true });

useSets key prefix changed from namespace: to sets:

When useSets is enabled, all keys (both data keys and the SET tracking key) now use a sets: prefix instead of namespace:. This prevents WRONGTYPE collisions between the SET tracking key and regular string data keys that could share the same name.

  • Data keys: sets:<namespace>::<key> (v5 stored the key as Keyv passed it, <namespace>:<key>)
  • SET tracking key: sets:<namespace> (was namespace:<namespace>)

The clear() method automatically detects and cleans up legacy namespace:-prefixed SET keys.

Data written by v5

v5's default setup stored foo as keyv:foo: the key Keyv had prefixed with its default keyv namespace. No v6 setting builds that key, so let those entries repopulate or rename them to the v6 layout. Data written with useRedisSets: false was stored as namespace:keyv:keyv:foo, which v6 reads with useSets: false, namespaceSeparator: ':', and new Keyv(store, { namespace: 'keyv:keyv' }). See the v5 to v6 migration guide for other setups.

Missing values are undefined, never null

get(), getMany(), and iterator() return undefined for missing keys. They never return null.

Constructor Options

KeyvValkey accepts a connection URI string, an options object, or an existing iovalkey Redis/Cluster instance. The options object accepts the following properties along with any RedisOptions from the iovalkey library:

Option Type Default Description
uri string undefined Valkey connection URI (redis:// or valkey://)
useSets boolean false Whether to use sets for namespace key management
namespace string undefined Namespace used to prefix keys for multi-tenant isolation
namespaceSeparator string '::' Separator between the namespace and the key, as in namespace:<namespace>::<key>

Properties

All configuration options are exposed as properties with getters and setters on the KeyvValkey instance. You can read or update them after construction.

capabilities

Read-only capability descriptor for the v6 storage contract. capabilities.expires is true, which means set() / setMany() accept an absolute Unix-ms expires timestamp.

  • Type: KeyvStorageCapability
const store = new KeyvValkey('redis://localhost:6379');
console.log(store.capabilities.expires); // true

namespace

Get or set the namespace for the adapter. Used for key prefixing and scoping operations like clear().

  • Type: string | undefined
  • Default: undefined
const store = new KeyvValkey('redis://localhost:6379');
store.namespace = 'my-namespace';
console.log(store.namespace); // 'my-namespace'

namespaceSeparator

Get or set the separator between the namespace and the key. Data keys are namespace:<namespace><separator><key>, or sets:<namespace><separator><key> with useSets: true.

  • Type: string
  • Default: '::'
const store = new KeyvValkey('redis://localhost:6379', { namespace: 'my-namespace' });
console.log(store.namespaceSeparator); // '::'
await store.set('foo', 'bar'); // stored as namespace:my-namespace::foo

useSets

Get or set whether to use sets for key management. When true, a set is maintained for each namespace to track keys. When false, keys are prefixed with the namespace and pattern matching is used instead.

  • Type: boolean
  • Default: false
const store = new KeyvValkey('redis://localhost:6379', { useSets: true });
console.log(store.useSets); // true

Note: When useSets is true, a set is maintained for each namespace which can lead to memory leaks in high-performance scenarios. This is why the default is false.

When useSets is enabled, all keys use the sets: prefix (e.g., sets:myns::mykey) to isolate them from non-useSets keys. The SET tracking key is stored at sets:<namespace>.

When useSets is false, the clear() function uses pattern matching (KEYS namespace:<namespace>::*, with any glob characters in the namespace and separator escaped) to find and delete keys, which may be slower on very large databases. A namespace that merely shares a prefix (for example users and users-archive) is not affected. A namespace that extends another with the separator (for example users::archive under users) cannot be told apart by the pattern and is cleared along with it. With no namespace this matches every key in the current database.

useSets: true doesn't keep such namespaces apart either. clear() removes only the keys the namespace's set tracks, but iterator() still matches sets:<namespace>::*, and the two namespaces build some of the same keys: archive::x in users and x in users::archive are both sets:users::archive::x. Keep the separator out of namespace names that have to stay apart.

useRedisSets (deprecated)

Deprecated alias for useSets. Use useSets instead.

client

Get or set the underlying iovalkey Redis or Cluster client instance. Replacing the client re-wires Hookified event listeners on the new instance and removes them from the previous one.

  • Type: Redis | Cluster
import Redis from 'iovalkey';

const store = new KeyvValkey('redis://localhost:6379');
console.log(store.client); // Redis instance

// Replace with a new instance
store.client = new Redis('redis://localhost:6380');

Methods

.get(key)

Returns the value for the given key. Returns undefined if the key does not exist. Never returns null.

const value = await store.get('foo');

.getMany(keys)

Returns an array of values for the given keys. Returns undefined for any key that does not exist. Never returns null entries. In cluster mode, keys are grouped by hash slot and fetched with per-slot MGET.

const values = await store.getMany(['foo', 'bar']);

.set(key, value, expires?)

Sets a value for the given key with an optional absolute expires (Unix ms since epoch). The adapter writes it via PXAT. Through Keyv (keyv.set(key, value, ttl)) you still pass a relative TTL in milliseconds — Keyv converts it to expires for you.

Returns true if the value was stored, or false if value is undefined or the write failed (the failure is also emitted as an error event).

await store.set('foo', 'bar');
await store.set('foo', 'bar', Date.now() + 5000); // expires in ~5 seconds

.setMany(entries)

Sets multiple key-value pairs in a single batch operation using MULTI/EXEC transactions. Each entry is a KeyvStorageEntry<Value> object ({ key: string, value: Value, expires?: number }) where expires is an absolute Unix ms timestamp, and Value is inferred from the entries provided. Entries with undefined values are skipped. Returns a boolean[] with per-entry success tracking by inspecting each command's result. In cluster mode, entries are grouped by hash slot with results mapped back to the original order.

const results = await store.setMany([
  { key: 'foo', value: 'bar' },
  { key: 'baz', value: 'qux', expires: Date.now() + 5000 },
]); // [true, true]

.delete(key)

Deletes a key-value pair from the store. Returns true if the key existed and was deleted, false otherwise.

const deleted = await store.delete('foo');

.deleteMany(keys)

Deletes multiple key-value pairs from the store. Each key is deleted individually (cluster-safe; not a single MULTI batch). Returns a boolean[] indicating whether each key was deleted.

const results = await store.deleteMany(['foo', 'bar']); // [true, true]

.has(key)

Returns true if the key exists in the store, false otherwise.

const exists = await store.has('foo');

.hasMany(keys)

Checks if multiple keys exist in the store in a single batch operation. In cluster mode, keys are grouped by hash slot. Returns an array of booleans.

const results = await store.hasMany(['foo', 'bar', 'baz']);
// [true, true, false]

.clear()

Clears all entries from the store. If a namespace is set, only entries within that namespace are cleared (namespace:<namespace>::*, glob characters escaped), so a namespace that merely shares a prefix such as users-archive is left alone. A namespace that extends it with the separator, such as users::archive, cannot be distinguished from keys containing the separator and is cleared too, so keep the separator out of namespace names that have to stay apart; useSets doesn't fully separate them (see useSets). If no namespace is set and useSets is false, this uses KEYS * and removes every key in the current database. In cluster mode every master node is searched.

await store.clear();

.iterator()

Returns an async iterator for iterating over all key-value pairs in the store. The iterator uses the namespace configured on the instance and the same key pattern as clear(), and in cluster mode it scans every master node. Missing values are yielded as undefined, never null.

for await (const [key, value] of store.iterator()) {
  console.log(key, value);
}

.disconnect()

Disconnects from the Valkey server. Subsequent operations on this adapter will throw.

await store.disconnect();

Events

KeyvValkey extends Hookified, so you can use on, once, off, and emit. The adapter re-emits the following events from the underlying iovalkey client:

Event Source Payload
error client error The Error from the client
connect client connect The iovalkey client instance
reconnecting client reconnecting Arguments from the client (e.g. delay)
disconnect client close The iovalkey client instance
const store = new KeyvValkey('redis://localhost:6379');

store.on('error', (error) => {
  console.error(error);
});

store.on('connect', (client) => {
  console.log('connected', client);
});

store.on('reconnecting', () => {
  console.log('reconnecting');
});

store.on('disconnect', () => {
  console.log('disconnected');
});

Replacing store.client removes listeners from the previous client and attaches them to the new one, so events are not duplicated or leaked.

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 with SET ... PXAT, the absolute-expiry option, so the deadline is immune to clock skew and to any latency between Keyv computing the expiry and Valkey receiving the command. The same applies to setMany, including the per-hash-slot grouping used in cluster mode.

Unlike the Redis adapter, there is no relative PX fallback and none is needed: PXAT was added in Redis 6.2, and every Valkey release is forked from Redis 7.2+, so absolute expiry is always available. You always call keyv.set(key, value, ttl) with a relative millisecond ttl (or rely on the ttl option); Keyv converts it to the absolute PXAT deadline for you.

Clustering

The adapter supports Valkey and Redis clusters via iovalkey's Cluster class. Pass a Redis.Cluster instance directly to the constructor:

import KeyvValkey from '@keyv/valkey';
import Redis from 'iovalkey';

const cluster = new Redis.Cluster([
  { host: '127.0.0.1', port: 7001 },
  { host: '127.0.0.1', port: 7002 },
  { host: '127.0.0.1', port: 7003 },
]);
const store = new KeyvValkey(cluster);

getMany, setMany, and hasMany automatically group keys by hash slot and run separate commands per slot group. This avoids CROSSSLOT errors without any extra configuration.

deleteMany deletes each key individually, which is also cluster-safe.

clear() and iterator() search every master node, since KEYS and SCAN only see the node they run on, and remove or fetch the keys they find one hash slot at a time.

Single-key methods (get, set, delete, has) work automatically in cluster mode — iovalkey routes each command to the correct node.

useSets in cluster mode

The tracking set and the data keys hash to different slots, which a cluster can't update in one MULTI transaction. In cluster mode the adapter updates them with separate commands instead. The commands run in an order that keeps every stored key in the set, even when another client writes, deletes or clears at the same time, or a command fails part-way:

  • set() and setMany() add keys to the set both before and after writing them.
  • delete() removes a key from the set between two UNLINKs. clear() does the same for every key it removes.

The set can end up listing keys that are no longer stored, which clear() removes harmlessly. Each set() and delete() takes three round trips instead of one transaction. Every write in a namespace also updates the one node that holds the namespace's set.

License

MIT © Jared Wray

Edit this page