
Security News
arXiv Is Rate Limiting Authors Following a Flood of AI Slop Submissions
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.
Affected versions:
Simple key-value storage with support for multiple backends
Keyv provides a consistent interface for key-value storage across multiple backends via storage adapters. It supports TTL based expiry, making it suitable as a cache or a persistent key-value store.
There are a few existing modules similar to Keyv, however Keyv is different because it:
Map APIBuffer and BigInt via the built-in KeyvJsonSerializererror events, so with a listener attached a database failure won't crash your appInstall Keyv.
npm install --save keyv
By default everything is stored in memory, you can optionally also install a storage adapter.
npm install --save @keyv/redis
npm install --save @keyv/valkey
npm install --save @keyv/mongo
npm install --save @keyv/sqlite
npm install --save @keyv/postgres
npm install --save @keyv/mysql
npm install --save @keyv/etcd
npm install --save @keyv/memcache
npm install --save @keyv/dynamo
First, create a new Keyv instance.
import Keyv from 'keyv';
You can create a Keyv instance with a generic type to enforce type safety for the values stored. Additionally, both the get and set methods support specifying custom types for specific use cases.
const keyv = new Keyv<number>(); // Instance handles only numbers
await keyv.set('key1', 123);
const value = await keyv.get('key1'); // value is inferred as number
You can also specify a type directly in the get or set methods, allowing flexibility for different types of values within the same instance.
const keyv = new Keyv(); // Generic type not specified at instance level
await keyv.set<string>('key2', 'some string'); // Method-level type for this value
const strValue = await keyv.get<string>('key2'); // Explicitly typed as string
await keyv.set<number>('key3', 456); // Storing a number in the same instance
const numValue = await keyv.get<number>('key3'); // Explicitly typed as number
This makes Keyv highly adaptable to different data types while maintaining type safety.
Once you have created your Keyv instance you can use it as a simple key-value store with in-memory by default. To use a storage adapter, create an instance of the adapter and pass it to the Keyv constructor. Here are some examples:
// redis
import KeyvRedis from '@keyv/redis';
const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'));
You can also pass in a storage adapter with other options such as ttl and namespace (example using sqlite):
//sqlite
import KeyvSqlite from '@keyv/sqlite';
const keyvSqlite = new KeyvSqlite('sqlite://path/to/database.sqlite');
const keyv = new Keyv({ store: keyvSqlite, ttl: 5000, namespace: 'cache' });
To handle an event you can do the following:
// Handle DB connection errors
keyv.on('error', err => console.log('Connection Error', err));
Now lets do an end-to-end example using Keyv and the Redis storage adapter:
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
const keyvRedis = new KeyvRedis('redis://user:pass@localhost:6379');
const keyv = new Keyv({ store: keyvRedis });
await keyv.set('foo', 'expires in 1 second', 1000); // true
await keyv.set('foo', 'never expires'); // true
await keyv.get('foo'); // 'never expires'
await keyv.delete('foo'); // true
await keyv.clear(); // undefined
It's is just that simple! Keyv is designed to be simple and easy to use.
You can namespace your Keyv instance to avoid key collisions and allow you to clear only a certain namespace while using the same database.
const users = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'), { namespace: 'users' });
const cache = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'), { namespace: 'cache' });
await users.set('foo', 'users'); // true
await cache.set('foo', 'cache'); // true
await users.get('foo'); // 'users'
await cache.get('foo'); // 'cache'
await users.clear(); // undefined
await users.get('foo'); // undefined
await cache.get('foo'); // 'cache'
You can also set the namespace on the storage adapter. Keyv uses it when Keyv has no namespace of its own. When both are set, Keyv's namespace wins and is applied to the adapter.
const users = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379', { namespace: 'users' }));
users.namespace; // 'users'
Keyv is an EventEmitter (built on hookified) and emits an 'error' event when an operation fails. Whether the operation also throws depends on whether a listener is attached. See Error Handling.
const keyv = new Keyv();
keyv.on('error', err => console.log('Connection Error', err));
In addition it will emit clear and disconnect events when the corresponding methods are called.
const keyv = new Keyv();
const handleConnectionError = err => console.log('Connection Error', err);
const handleClear = () => console.log('Cache Cleared');
const handleDisconnect = () => console.log('Disconnected');
keyv.on('error', handleConnectionError);
keyv.on('clear', handleClear);
keyv.on('disconnect', handleDisconnect);
Keyv handles errors the way a Node.js EventEmitter does. When an operation fails, for example because the storage adapter cannot reach its database, Keyv emits an 'error' event:
'error' listener attached, the listener receives the error and the operation returns a fallback value instead of throwing.'error' listener attached, the operation rejects with the error.const keyv = new Keyv(store); // any storage adapter
// No listener yet: a failed call rejects.
try {
await keyv.get('foo');
} catch (error) {
console.error('get failed', error);
}
// With a listener: the listener gets the error and the call returns a fallback value.
keyv.on('error', (error) => console.error('Keyv error', error));
await keyv.get('foo'); // undefined if the store fails
These are the fallback values a failed call returns when a listener is attached:
| Method | Returns |
|---|---|
get, getRaw | undefined |
getMany, getManyRaw | an array of undefined |
set, setRaw, delete, has | false |
setMany, setManyRaw, deleteMany, hasMany | an array of false |
clear, disconnect | undefined |
iterator | ends the iteration |
A failed read looks the same as a missing key, so use the 'error' events when you need to tell them apart. In stats, a failed read counts as an error, not a miss.
Storage adapters can also emit 'error' on their own, outside any Keyv call. The Redis client does this when a connection drops, for example. Keyv forwards these errors to its own 'error' event, and the same rule applies: with no listener attached the error is thrown, and because no call is waiting for it, it can crash your process. Attach an 'error' listener whenever you use a storage adapter that connects to a server.
Keyv v5's throwOnErrors and emitErrors options were removed in v6. See the v5 to v6 migration guide for how v5 behaved and what to change.
Keyv supports hooks for all of its operations. Hooks are useful for logging, debugging, and other custom functionality. Each operation fires a BEFORE_* hook before it runs and an AFTER_* hook after it completes. Here is the list of all the hooks:
BEFORE_GET / AFTER_GET
BEFORE_GET_MANY / AFTER_GET_MANY
BEFORE_GET_RAW / AFTER_GET_RAW
BEFORE_GET_MANY_RAW / AFTER_GET_MANY_RAW
BEFORE_SET / AFTER_SET
BEFORE_SET_RAW / AFTER_SET_RAW
BEFORE_SET_MANY / AFTER_SET_MANY
BEFORE_SET_MANY_RAW / AFTER_SET_MANY_RAW
BEFORE_DELETE / AFTER_DELETE
BEFORE_DELETE_MANY / AFTER_DELETE_MANY
BEFORE_HAS / AFTER_HAS
BEFORE_HAS_MANY / AFTER_HAS_MANY
BEFORE_CLEAR / AFTER_CLEAR
BEFORE_DISCONNECT / AFTER_DISCONNECT
The older
PRE_*/POST_*hook names (e.g.PRE_GET,POST_SET) are deprecated aliases that still fire for backward compatibility. Prefer theBEFORE_*/AFTER_*names going forward.
You can access these by importing KeyvHooks from the main Keyv package and registering a handler with onHook():
import Keyv, { KeyvHooks } from 'keyv';
const keyv = new Keyv();
keyv.onHook(KeyvHooks.BEFORE_SET, (data) => {
console.log(`Setting key ${data.key} to ${data.value}`);
});
The AFTER_GET and AFTER_GET_RAW hooks fire on both cache hits and misses. When a cache miss occurs (key doesn't exist or is expired), the hook receives undefined as the value.
// AFTER_GET hook - fires on both hits and misses
const keyv = new Keyv();
keyv.onHook(KeyvHooks.AFTER_GET, (data) => {
if (data.value === undefined) {
console.log(`Cache miss for key: ${data.key}`);
} else {
console.log(`Cache hit for key: ${data.key}`, data.value);
}
});
await keyv.get('existing-key'); // Logs cache hit with value
await keyv.get('missing-key'); // Logs cache miss with undefined
// AFTER_GET_RAW hook - same behavior as AFTER_GET
const keyv = new Keyv();
keyv.onHook(KeyvHooks.AFTER_GET_RAW, (data) => {
console.log(`Key: ${data.key}, Value:`, data.value);
});
await keyv.getRaw('foo'); // Logs with value or undefined
// BEFORE_SET hook
const keyv = new Keyv();
keyv.onHook(KeyvHooks.BEFORE_SET, (data) => console.log(`Setting key ${data.key} to ${data.value}`));
// AFTER_SET hook
const keyv = new Keyv();
keyv.onHook(KeyvHooks.AFTER_SET, ({ key, value }) => console.log(`Set key ${key} to ${value}`));
In the BEFORE_SET hook you can also manipulate the value before it is set. For example, you could add a prefix to all keys.
const keyv = new Keyv();
keyv.onHook(KeyvHooks.BEFORE_SET, (data) => {
console.log(`Manipulating key ${data.key} and ${data.value}`);
data.key = `prefix-${data.key}`;
data.value = `prefix-${data.value}`;
});
Now this key will have prefix- added to it before it is set.
In the BEFORE_DELETE and AFTER_DELETE hooks, the value could be a single item or an Array. This is based on the fact that delete can accept a single key or an Array of keys.
By default, Keyv uses its built-in KeyvJsonSerializer — a JSON-based serializer with support for Buffer and BigInt types. This works out of the box with all storage adapters.
In addition to the built-in serializer, Keyv offers two official serialization packages:
@keyv/serialize-superjson supports Date, RegExp, Map, Set, BigInt, undefined, Error, and URL types.
import Keyv from 'keyv';
import { superJsonSerializer } from '@keyv/serialize-superjson'; // using the helper function that does new KeyvSuperJsonSerializer()
const keyv = new Keyv({ serialization: superJsonSerializer });
@keyv/serialize-msgpackr is a binary serializer that supports Date, RegExp, Map, Set, Error, undefined, NaN, and Infinity types.
import Keyv from 'keyv';
import { KeyvMsgpackrSerializer } from '@keyv/serialize-msgpackr';
const keyv = new Keyv({ serialization: new KeyvMsgpackrSerializer() });
You can provide your own serializer by implementing the KeyvSerializationAdapter interface with stringify and parse methods:
interface KeyvSerializationAdapter {
stringify: (object: unknown) => string | Promise<string>;
parse: <T>(data: string) => T | Promise<T>;
}
You can disable serialization entirely by passing false. This stores data as raw objects, which works for in-memory Map storage where string conversion is not needed:
const keyv = new Keyv({ serialization: false });
When serialization, compression, and/or encryption are configured, Keyv applies them in this order:
On set: serialize → compress (optional) → encrypt (optional) → store
On get: store → decrypt (optional) → decompress (optional) → parse → value
Compression and encryption operate on the serialized string, so they only run when a serializer is configured. The built-in KeyvJsonSerializer is enabled by default, so this works out of the box. If you disable serialization with serialization: false, values are passed through to the store as-is and compression is skipped. Encryption isn't skipped: with an encryption adapter set, every write fails instead of storing the value unencrypted. Keyv emits error, and set() returns false when a listener is attached or rejects when none is.
The official storage adapters are covered by over 150 integration tests to guarantee consistent behaviour. They are lightweight, efficient wrappers over the DB clients making use of indexes and native TTLs where available.
| Database | Adapter | Native TTL |
|---|---|---|
| Redis | @keyv/redis | Yes |
| Valkey | @keyv/valkey | Yes |
| MongoDB | @keyv/mongo | Yes |
| SQLite | @keyv/sqlite | No |
| PostgreSQL | @keyv/postgres | No |
| MySQL | @keyv/mysql | No |
| Etcd | @keyv/etcd | Yes |
| Memcache | @keyv/memcache | Yes |
| DynamoDB | @keyv/dynamo | Yes |
We love the community and the third-party storage adapters they have built. They enable Keyv to be used with even more backends and use cases.
You can also use third-party storage adapters or build your own. Keyv will wrap these storage adapters in TTL functionality and handle complex types internally.
import Keyv from 'keyv';
import myAdapter from 'my-adapter';
const keyv = new Keyv({ store: myAdapter });
Any store that follows the Map api will work.
new Keyv({ store: new Map() });
For example, quick-lru is a completely unrelated module that implements the Map API.
import Keyv from 'keyv';
import QuickLRU from 'quick-lru';
const lru = new QuickLRU({ maxSize: 1000 });
const keyv = new Keyv({ store: lru });
View the complete list of third-party storage adapters and learn how to build your own at https://keyv.org/docs/storage-adapters/third-party/
The public API above is unchanged —
keyv.set(key, value, ttl)still takes a relative TTL in milliseconds. The change below only affects authors of custom storage adapters.
As of v6, Keyv passes an absolute expires timestamp (Unix ms since epoch) to a storage adapter's write methods instead of a relative TTL. Keyv computes expires once, so adapters never need to derive or parse it:
import { keyvStorageCapability } from 'keyv';
type KeyvStorageEntry<Value> = { key: string; value: Value; expires?: number };
class MyAdapter {
// Declare support for the absolute-`expires` contract:
get capabilities() {
return keyvStorageCapability(this); // -> { ...detected, expires: true }
}
// `expires` is absolute Unix ms; `undefined` means no expiry; `<= Date.now()` means already expired.
async set(key: string, value: unknown, expires?: number): Promise<boolean> { /* ... */ }
async setMany<Value>(entries: KeyvStorageEntry<Value>[]): Promise<boolean[] | undefined> { /* ... */ }
// ...get, delete, clear, has, getMany, deleteMany, hasMany, etc.
}
A v6 adapter declares capabilities.expires === true (the keyvStorageCapability(this) helper sets it for you). Keyv then passes the absolute expires to it directly — this takes precedence over structural detection, so an adapter whose methods aren't written with async is still used directly rather than bridged. Any legacy storage adapter that does not declare capabilities.expires is treated as a relative-TTL adapter and transparently wrapped by KeyvBridgeAdapter, which converts the absolute expires back to a relative TTL before delegating (and deletes outright when the deadline has already elapsed) — so existing third-party adapters keep working unchanged. Stores that expose absolute-expiry primitives (e.g. Redis PXAT) use expires directly. Map-like stores wrapped via new Keyv({ store: new Map() }) are unaffected.
Adapters should enforce expiry — and Keyv double-checks by default. Declaring
capabilities.expires === truemeans a v6 adapter should enforce expiry itself — ideally via a native mechanism (key expiry, TTL index, lease) so the backend reclaims space, and/or a client-side check on read. On top of that, Keyv core filters expired reads at its own layer by default (checkExpiredistrue), using the absoluteexpiresin the serialized envelope, soget/getMany/hasnever surface a key past its deadline even on backends whose native expiry is coarse or lazily swept (e.g. Memcached, DynamoDB). Run@keyv/test-suite'sstorageTtlTestsagainst your adapter to verify its own expiry behaviour.
Report each failure once. When an operation fails, an adapter should either reject, and Keyv then emits the error, or emit
'error'itself and return a fallback value. It should not do both: Keyv forwards the adapter's'error'events, so a failure that is emitted and then rejected reaches Keyv's listeners twice. Errors that happen outside any call, such as a dropped connection, should be emitted. See Error Handling.
Keyv ships with two storage adapters built directly into the core package. You rarely instantiate them yourself — Keyv selects and wires up the right one automatically when you create an instance — but knowing how they work explains how the default in-memory store behaves and how legacy or async stores are adapted to the v6 contract. Both are exported from keyv:
KeyvMemoryAdapter is the default store. When you create a Keyv instance without a store (new Keyv()), it uses new KeyvMemoryAdapter(new Map()) under the hood. It wraps any synchronous, Map-like object — the built-in Map, quick-lru, lru.min, or anything exposing get, set, delete, clear, and has.
It adds the pieces a raw Map does not have:
namespace and namespaceSeparator (default ::), so one underlying store can host multiple namespaces. A namespaced clear() removes only the current namespace's keys, which it finds with the underlying store's keys() method (a standard Map has one). A minimal store without keys() can't tell namespaces apart, so a namespaced clear() throws instead of wiping the entire store; call clear() without a namespace to empty it.{ value, expires } wrapper alongside the stored value, so it can evict expired entries lazily on get, getMany, has, and iterator() without decoding the value. This wrapper is separate from the { value, expires } envelope Keyv core builds and runs through serialization/compression/encryption — with the default serializer that encoded payload still contains expires, so custom serializer/encryption adapters must still handle the expires field; only the adapter's outer copy lives outside the payload. When the underlying store accepts a TTL argument (e.g. QuickLRU), the adapter also passes a derived relative duration so the store can evict on its own.getMany, setMany, hasMany, deleteMany, and an async iterator() (when the store supports entries()).capabilities.expires === true, so Keyv hands it the absolute expires timestamp directly and trusts it to enforce expiry.import Keyv, { KeyvMemoryAdapter } from 'keyv';
// Wrap any Map-like store. Set the namespace on Keyv or on the adapter. When both are set,
// Keyv's namespace wins and is applied to the adapter.
const keyv = new Keyv({ store: new KeyvMemoryAdapter(new Map()), namespace: 'cache' });
// Or wrap an LRU to bound memory usage
import QuickLRU from 'quick-lru';
const cache = new Keyv({ store: new KeyvMemoryAdapter(new QuickLRU({ maxSize: 1000 })) });
KeyvBridgeAdapter wraps any promise-based / async store and adapts it to the v6 storage contract. Keyv applies it automatically when you pass:
capabilities.expires (i.e. pre-v6 third-party adapters), orMap-like store with async get, set, delete, and clear. For such a store to actually expire data, its set(key, value, ttl) must honor the relative millisecond ttl the bridge passes as the third argument — a plain Promise-wrapped Map that ignores it won't evict on its own. Keyv's checkExpired (on by default) still filters expired entries on read, but they linger in the store until read; for native eviction, prefer a full v6 adapter or a store that honors the ttl.A method that isn't a native async function can still return a promise, such as an async method compiled to an older target or one written without async. So when a store isn't a Map and its methods aren't native async functions, Keyv calls its has once when the store is set. If that returns a promise, the store goes through the bridge. Otherwise Keyv wraps it in KeyvMemoryAdapter.
This is why existing third-party adapters keep working unchanged on v6. The bridge:
expires timestamp; the bridge converts it back to the relative TTL the wrapped store expects. A write whose deadline has already elapsed is deleted instead of stored, so a past expires becomes an absent key — matching how the native adapters treat an already-expired write.getMany, setMany, has, hasMany, deleteMany, iterator, or disconnect, the bridge calls them directly; otherwise it falls back to looping over the single-key primitives.namespace property), the bridge propagates its namespace to the store (or keeps the store's own namespace when the bridge has none) and does not prefix keys, avoiding double-namespacing, so the store's native scoped clear() and iterator() are used. Otherwise the bridge prefixes keys itself, letting one shared store host multiple namespaces. A namespaced clear() then finds the namespace's keys with the store's iterator(); a store without one can't tell namespaces apart, so clear() throws instead of wiping every namespace.error events from the wrapped store so connection failures surface on the Keyv instance.import Keyv, { KeyvBridgeAdapter } from 'keyv';
// Usually automatic — just pass the store:
const keyv = new Keyv({ store: myAsyncStore });
// ...which is equivalent to wrapping it explicitly. A namespace on the Keyv options is applied
// to the adapter. Without one, Keyv keeps the adapter's own namespace:
const explicit = new Keyv({ store: new KeyvBridgeAdapter(myAsyncStore), namespace: 'cache' });
JavaScript's built-in Map object has a practical limit of approximately 16.7 million entries (2^24). When you try to store more entries than this limit, you'll encounter performance degradation or runtime errors. This limitation is due to how JavaScript engines internally manage Map objects.
For applications that need to cache millions of entries in memory, this becomes a significant constraint. Common scenarios include:
@keyv/bigmap solves this limitation by using a distributed hash approach with multiple internal Map instances. Instead of storing all entries in a single Map, BigMap distributes entries across multiple Maps using a hash function. This allows you to scale beyond the 16.7 million entry limit while maintaining the familiar Map API.
BigMap can be used directly with Keyv as a storage adapter, providing scalable in-memory storage with full TTL support.
npm install --save keyv @keyv/bigmap
The simplest way to use BigMap with Keyv is through the createKeyv helper function:
import { createKeyv } from '@keyv/bigmap';
const keyv = createKeyv();
// Set values with TTL (time in milliseconds)
await keyv.set('user:1', { name: 'Alice', email: 'alice@example.com' }, 60000); // Expires in 60 seconds
// Get values
const user = await keyv.get('user:1');
console.log(user); // { name: 'Alice', email: 'alice@example.com' }
// Delete values
await keyv.delete('user:1');
// Clear all values
await keyv.clear();
For more details about BigMap, see the @keyv/bigmap documentation.
Keyv supports gzip, brotli and lz4 compression. To enable compression, pass the compression option to the constructor. Compression runs on the serialized value, so it requires a serializer — the built-in KeyvJsonSerializer is enabled by default. With serialization: false, values are stored uncompressed.
import Keyv from 'keyv';
import KeyvGzip from '@keyv/compress-gzip';
const keyvGzip = new KeyvGzip();
const keyv = new Keyv({ compression: keyvGzip });
import Keyv from 'keyv';
import KeyvBrotli from '@keyv/compress-brotli';
const keyvBrotli = new KeyvBrotli();
const keyv = new Keyv({ compression: keyvBrotli });
import Keyv from 'keyv';
import KeyvLz4 from '@keyv/compress-lz4';
const keyvLz4 = new KeyvLz4();
const keyv = new Keyv({ compression: keyvLz4 });
You can also pass a custom compression function to the compression option. Following the pattern of the official compression adapters.
Great! Keyv is designed to be easily extended. You can build your own compression adapter by following the pattern of the official compression adapters based on this interface:
interface KeyvCompressionAdapter {
compress(value: string): Promise<string>;
decompress(value: string): Promise<string>;
}
In addition to the interface, you can test it with our compression test suite using @keyv/test-suite:
import { compressionTestSuite } from '@keyv/test-suite';
import { it } from 'vitest';
import KeyvGzip from '@keyv/compress-gzip';
compressionTestSuite(it, new KeyvGzip());
Keyv supports pluggable encryption of stored values via the KeyvEncryptionAdapter interface. Pass an adapter with encrypt and decrypt methods using the encryption option (or set the .encryption property). Encryption runs on the serialized (and optionally compressed) value, so it requires a serializer — the built-in KeyvJsonSerializer is enabled by default. With serialization: false, writes fail with an error instead of storing values unencrypted.
interface KeyvEncryptionAdapter {
encrypt: (data: string) => string | Promise<string>;
decrypt: (data: string) => string | Promise<string>;
}
import Keyv from 'keyv';
const encryption = {
encrypt: async (data) => Buffer.from(data).toString('base64'),
decrypt: async (data) => Buffer.from(data, 'base64').toString('utf8'),
};
const keyv = new Keyv({ encryption });
await keyv.set('foo', 'bar'); // value is encrypted at rest
await keyv.get('foo'); // 'bar'
Keyv exports helper functions to check whether an object implements the expected interface for a Keyv instance, storage adapter, compression adapter, serialization adapter, or encryption adapter. Each function returns an object with a top-level compatible boolean (whether the object fully satisfies the interface) plus a methods record describing every method it looked for.
import {
detectKeyv,
detectKeyvStorage,
detectKeyvCompression,
detectKeyvSerialization,
detectKeyvEncryption,
} from 'keyv';
Every entry in the methods record has the shape { exists: boolean, methodType: "sync" | "async" | "none" }.
Returns a KeyvCapability: { compatible, methods, properties }. compatible is true only when all Keyv methods and properties are present.
import Keyv, { detectKeyv } from 'keyv';
const result = detectKeyv(new Keyv());
result.compatible; // true — all capabilities present
result.methods.get.exists; // true
result.methods.get.methodType; // "async"
result.properties.hooks; // true
result.properties.stats; // true
const partial = detectKeyv(new Map());
partial.compatible; // false — missing getMany, setMany, hooks, stats, etc.
partial.methods.get.exists; // true
Returns a KeyvStorageCapability: { compatible, store, methods }. compatible is true when the object is a usable storage adapter, and store reports the detected kind:
"keyvStorage" — implements the full async storage adapter interface (get, set, delete, clear, has, setMany, deleteMany, hasMany, all async)"mapLike" — has synchronous get, set, delete, and has (i.e. it behaves like a Map)"asyncMap" — has at least async get, set, delete, and clear"none" — not a usable storemethodType is "async" only for native async functions, so a store whose methods return promises without being async is reported as "mapLike". Keyv still sends such a store through KeyvBridgeAdapter.
import { detectKeyvStorage } from 'keyv';
// Map-like object
const map = detectKeyvStorage(new Map());
map.compatible; // true
map.store; // "mapLike"
map.methods.get.methodType; // "sync"
// Async storage adapter
const adapter = {
get: async () => {}, set: async () => {}, delete: async () => {},
clear: async () => {}, has: async () => {}, setMany: async () => {},
deleteMany: async () => {}, hasMany: async () => {},
};
const adapterResult = detectKeyvStorage(adapter);
adapterResult.compatible; // true
adapterResult.store; // "keyvStorage"
adapterResult.methods.get.methodType; // "async"
Returns a KeyvCompressionCapability: { compatible, methods }. compatible is true when both compress and decompress methods are present.
import { detectKeyvCompression } from 'keyv';
const result = detectKeyvCompression({ compress: (d) => d, decompress: (d) => d });
result.compatible; // true
result.methods.compress.exists; // true
result.methods.decompress.exists; // true
Returns a KeyvSerializationCapability: { compatible, methods }. compatible is true when both stringify and parse methods are present.
import { detectKeyvSerialization } from 'keyv';
const result = detectKeyvSerialization(JSON);
result.compatible; // true
result.methods.stringify.exists; // true
result.methods.parse.exists; // true
Returns a KeyvEncryptionCapability: { compatible, methods }. compatible is true when both encrypt and decrypt methods are present.
import { detectKeyvEncryption } from 'keyv';
const result = detectKeyvEncryption({ encrypt: (d) => d, decrypt: (d) => d });
result.compatible; // true
result.methods.encrypt.exists; // true
result.methods.decrypt.exists; // true
Returns a new Keyv instance.
The Keyv instance is also an EventEmitter that will emit an 'error' event if an operation or the storage adapter connection fails. See Error Handling.
Type: KeyvStorageAdapter
Default: undefined
The storage adapter instance to be used by Keyv.
Type: String
Default: undefined
This is the namespace for the current instance. When you set it, Keyv also sets it on the storage adapter. When Keyv has no namespace of its own, this returns the namespace set on the storage adapter.
Type: Object
The options object is also passed through to the storage adapter. Check your storage adapter docs for any extra options.
Type: String
Default: undefined
Namespace for the current instance. When omitted, Keyv uses the namespace set on the storage adapter, if any.
Type: Number
Default: undefined
Default TTL. Can be overridden by specififying a TTL on .set().
Type: KeyvCompressionAdapter
Default: undefined
Compression package to use. See Compression for more details.
Type: KeyvSerializationAdapter | false
Default: KeyvJsonSerializer (built-in)
A serialization object with stringify and parse methods. Set to false to disable serialization and store raw objects. See Serialization for more details.
Type: Storage adapter instance
Default: new Map()
The storage adapter instance to be used by Keyv.
Type: Boolean
Default: false
Enable statistics tracking (hits, misses, sets, deletes, errors). See .stats for details.
Type: KeyvSanitizeOptions
Default: undefined
Enable sanitization of keys and namespaces by stripping dangerous patterns. See .sanitize for details.
Type: KeyvEncryptionAdapter
Default: undefined
Encryption adapter used to encrypt and decrypt stored values. See Encryption for details.
Type: Boolean
Default: true
When true (default), Keyv checks expiry at its own layer on get/getMany/has/hasMany in addition to the storage adapter. Set to false to trust the storage adapter alone. See .checkExpired for details.
Keys must always be strings. Values can be of any type.
Set a value.
By default keys are persistent. You can set an expiry TTL in milliseconds. A fractional TTL is rounded up to a whole millisecond, since stores such as PostgreSQL and Redis only accept whole-millisecond expiry times.
Returns a promise which resolves to true.
Set multiple values using KeyvEntry<Value> objects ({ key: string, value: Value, ttl?: number }). The Value type is inferred from the entries provided.
Returns a promise which resolves to the retrieved value, or undefined if the key does not exist or is expired. If an array of keys is passed it delegates to .getMany() and resolves to an array of values.
Returns a promise which resolves to an array of retrieved values, with undefined for keys that do not exist or are expired.
Returns a promise which resolves to the raw stored data for the key or undefined if the key does not exist or is expired.
Returns a promise which resolves to an array of raw stored data for the keys or undefined if the key does not exist or is expired.
Sets a raw value in the store without wrapping. This is the write-side counterpart to .getRaw(). The caller provides the KeyvValue envelope directly ({ value, expires? }) instead of having Keyv wrap it. The envelope is still serialized before storing so that all read paths (get(), getRaw(), has(), getManyRaw()) work consistently. If you need TTL-based expiration, set expires on the value directly (e.g. { value: 'bar', expires: Date.now() + 60000 }). The store-level TTL is derived automatically from value.expires, rounded up to a whole millisecond. An expires that isn't a finite number gives no store-level expiry.
Returns a promise which resolves to true.
const keyv = new Keyv();
// Set a raw value with expiration
await keyv.setRaw('foo', { value: 'bar', expires: Date.now() + 60000 });
// Set a raw value without expiration
await keyv.setRaw('foo', { value: 'bar' });
// Round-trip: get raw, modify, set raw
const raw = await keyv.getRaw('foo');
if (raw) {
raw.value = 'updated';
await keyv.setRaw('foo', raw);
}
Sets many raw values in the store without wrapping. Each entry should have a key and a value (KeyvValue envelope). Like setRaw(), the envelopes are serialized before storing and the store-level TTL is derived from each entry's value.expires.
Returns a promise which resolves to an array of booleans.
const keyv = new Keyv();
await keyv.setManyRaw([
{ key: 'foo', value: { value: 'bar' } },
{ key: 'baz', value: { value: 'qux', expires: Date.now() + 60000 } },
]);
Deletes an entry.
Returns a promise which resolves to true if the key existed, false if not.
Deletes multiple entries.
Returns a promise which resolves to true if all keys were deleted successfully, false otherwise.
Delete all entries in the current namespace.
Returns a promise which is resolved when the entries have been cleared.
If the store can't limit the delete to the namespace, nothing is deleted and Keyv emits error instead. That happens with a Map-like store that has no keys(), and with an older async store that has no iterator() and doesn't manage its own namespace.
Check if a key exists in the store.
Returns a promise which resolves to true if the key exists, false if not.
await keyv.set('foo', 'bar');
await keyv.has('foo'); // true
await keyv.has('unknown'); // false
Check if multiple keys exist in the store.
Returns a promise which resolves to an array of booleans indicating if each key exists.
await keyv.set('foo', 'bar');
await keyv.hasMany(['foo', 'unknown']); // [true, false]
Disconnect from the storage adapter. Emits a 'disconnect' event.
Returns a promise which is resolved when the connection has been closed.
await keyv.disconnect();
Iterate over all key-value pairs in the store. Automatically deserializes values, filters out expired entries, and deletes them.
Returns an async generator that yields [key, value] pairs. Use with for await...of:
for await (const [key, value] of keyv.iterator()) {
console.log(key, value);
}
The iterator works with any storage backend:
Symbol.iteratoriterator() method (e.g., Redis SCAN, SQL cursor)Type: String
The namespace for the current instance. This will define the namespace for the current instance and the storage adapter. If you set the namespace to undefined it will no longer do key prefixing. When Keyv has no namespace of its own, it uses the namespace set on the storage adapter, and this property returns it.
const keyv = new Keyv({ namespace: 'my-namespace' });
console.log(keyv.namespace); // 'my-namespace'
here is an example of setting the namespace to undefined:
const keyv = new Keyv();
console.log(keyv.namespace); // undefined which is default
keyv.namespace = undefined;
console.log(keyv.namespace); // undefined
Type: Number
Default: undefined
Default TTL. Can be overridden by specififying a TTL on .set(). If set to undefined it will never expire.
const keyv = new Keyv({ ttl: 5000 });
console.log(keyv.ttl); // 5000
keyv.ttl = undefined;
console.log(keyv.ttl); // undefined (never expires)
Type: Storage adapter instance
Default: new Map()
The storage adapter instance to be used by Keyv. This will wire up the iterator, events, and more when a set happens. If it is not a valid Map or Storage Adapter it will throw an error.
import KeyvSqlite from '@keyv/sqlite';
const keyv = new Keyv();
console.log(keyv.store instanceof Map); // true
keyv.store = new KeyvSqlite('sqlite://path/to/database.sqlite');
console.log(keyv.store instanceof KeyvSqlite); // true
Type: KeyvSerializationAdapter | false | undefined
Default: KeyvJsonSerializer (built-in)
The serialization object used for storing and retrieving values. Set to false or undefined to disable serialization and use raw object pass-through. See Serialization for more details.
const keyv = new Keyv();
console.log(keyv.serialization); // KeyvJsonSerializer (default)
keyv.serialization = false; // disable serialization
console.log(keyv.serialization); // undefined
Type: KeyvCompressionAdapter
Default: undefined
This is the compression package to use. See Compression for more details. If it is undefined it will not compress (default).
import KeyvGzip from '@keyv/compress-gzip';
const keyv = new Keyv();
console.log(keyv.compression); // undefined
keyv.compression = new KeyvGzip();
console.log(keyv.compression); // KeyvGzip
Type: KeyvEncryptionAdapter
Default: undefined
The encryption adapter used to encrypt and decrypt stored values. If undefined (default) values are not encrypted. See Encryption for more details.
const keyv = new Keyv();
console.log(keyv.encryption); // undefined
keyv.encryption = {
encrypt: async (data) => Buffer.from(data).toString('base64'),
decrypt: async (data) => Buffer.from(data, 'base64').toString('utf8'),
};
console.log(keyv.encryption); // the encryption adapter
Type: Boolean
Default: true
A read-only property (configured via the checkExpired constructor option). When true (the default), Keyv checks expiry at its own layer on get, getMany, has, and hasMany, deleting any expired entries it encounters. It does this using the absolute expires stored in the serialized envelope, so reads stay millisecond-precise regardless of the adapter.
This defaults to true because some backends can return entries that are already logically expired: Memcached's exptime is second-granular (a value can linger up to ~1s past a sub-second deadline), and DynamoDB's native TTL is a background sweep that can lag by hours before it deletes expired items. Trusting the backend alone would surface those stale reads; the Keyv-layer check closes that gap.
Set it to false to trust the storage adapter to handle expiry on its own. That skips the extra decode + expiry check on every read (and lets has/hasMany use the adapter's native existence check), at the cost of backend-granularity expiry.
const keyv = new Keyv();
console.log(keyv.checkExpired); // true (default)
const trusting = new Keyv({ checkExpired: false });
console.log(trusting.checkExpired); // false
Type: KeyvStats
Default: KeyvStats instance with enabled: false
The stats property provides access to statistics tracking for cache operations. When enabled via the stats option during initialization, it tracks hits, misses, sets, deletes, and errors. It also maintains LRU-bounded per-key frequency maps for each event type, allowing you to see which keys are accessed most.
const keyv = new Keyv({ stats: true });
console.log(keyv.stats.enabled); // true
Aggregate counters:
hits: Number of successful cache retrievalsmisses: Number of reads that found no value because the key was missing or expired. A read that fails counts in errors instead.sets: Number of set operationsdeletes: Number of delete operationserrors: Number of errors encounteredPer-key LRU frequency maps (each capped at maxEntries, default 1000):
hitKeys: Map<string, number> — key to hit countmissKeys: Map<string, number> — key to miss countsetKeys: Map<string, number> — key to set countdeleteKeys: Map<string, number> — key to delete counterrorKeys: Map<string, number> — key to error countconst keyv = new Keyv({ stats: true });
await keyv.set('foo', 'bar');
await keyv.get('foo'); // cache hit
await keyv.get('nonexistent'); // cache miss
await keyv.delete('foo');
console.log(keyv.stats.hits); // 1
console.log(keyv.stats.misses); // 1
console.log(keyv.stats.sets); // 1
console.log(keyv.stats.deletes); // 1
// Per-key frequency maps
console.log(keyv.stats.hitKeys.get('foo')); // 1
console.log(keyv.stats.missKeys.get('nonexistent')); // 1
keyv.stats.reset();
console.log(keyv.stats.hits); // 0
console.log(keyv.stats.hitKeys.size); // 0
You can also manually enable/disable stats tracking at runtime. Disabling stats will automatically unsubscribe from events:
const keyv = new Keyv({ stats: false });
keyv.stats.enabled = true; // Enable stats tracking
// ... perform operations ...
keyv.stats.enabled = false; // Disable stats tracking and unsubscribe
You can create a KeyvStats instance independently and subscribe it to a Keyv instance:
import { KeyvStats } from 'keyv';
const stats = new KeyvStats({ enabled: true, maxEntries: 500, emitter: keyv });
Type: KeyvSanitize (configured via the sanitize option: KeyvSanitizeOptions)
Default: disabled
The .sanitize property is a KeyvSanitize adapter. It is configured through the sanitize constructor option, a KeyvSanitizeOptions object, and is disabled by default.
It detects and strips dangerous patterns from keys and namespaces to protect against SQL injection, MongoDB operator injection, path traversal, and control character attacks. Harmless characters like quotes, slashes, and dollar signs pass through unchanged — only dangerous patterns are stripped. Stripping repeats until nothing matches, so ..././etc becomes etc rather than ../etc.
Results are cached in an LRU cache (10,000 entries) for fast repeated lookups.
| Category | Patterns Stripped | Purpose |
|---|---|---|
sql | ; -- /* | Prevents SQL injection |
mongo | leading $, {$ sequences | Prevents MongoDB operator injection |
escape | \0 \r \n | Strips null bytes, CRLF injection |
path | ../ ..\ | Prevents path traversal |
| Target | Default | Description |
|---|---|---|
keys | true (when enabled) | Sanitize keys on all operations |
namespace | true (when enabled) | Sanitize namespace on construction and setter |
Enable all sanitization:
const keyv = new Keyv({ sanitize: { keys: true, namespace: true } });
await keyv.set("user;1--", "value");
// Key is stored as "user1"
// Harmless characters pass through
await keyv.set("user's-data", "value");
// Key is stored as "user's-data" (unchanged)
Disable all sanitization (default) by omitting sanitize, or by setting both targets to false:
const keyv = new Keyv({ sanitize: { keys: false, namespace: false } });
Granular control per target and category:
const keyv = new Keyv({
sanitize: {
keys: { sql: true, mongo: false }, // only SQL patterns on keys
namespace: { path: true, sql: false }, // only path patterns on namespace
}
});
Disable namespace sanitization only:
const keyv = new Keyv({
sanitize: { keys: true, namespace: false }
});
Change at runtime by updating the options on the existing adapter, or by replacing it:
import { KeyvSanitize } from 'keyv';
// Update options on the existing adapter
keyv.sanitize.updateOptions({ keys: true, namespace: true }); // enable all
keyv.sanitize.updateOptions({ keys: { sql: true, mongo: false } }); // granular
// Or replace the adapter entirely
keyv.sanitize = new KeyvSanitize({ keys: true, namespace: true });
Sanitization is applied to all key-accepting methods: get, set, delete, has, getMany, setMany, deleteMany, hasMany, getRaw, getManyRaw, setRaw, and setManyRaw. Namespace sanitization is applied at construction and when the namespace setter is used.
A key that is empty after sanitization never reaches the store. get and getRaw return undefined, set, setRaw, delete, and has return false, and the batch methods put undefined or false in that key's position.
A namespace that is empty after sanitization, such as $$ or ;, becomes keyv-sanitized. An empty namespace would turn namespacing off, mixing the instance's keys with unrelated ones and letting clear() remove them.
We make a best effort to support Bun as a runtime. Our default and primary target is Node.js, but we run tests against Bun to ensure compatibility. If you encounter any issues while using Keyv with Bun, please report them at our GitHub issues.
We welcome contributions to Keyv! 🎉 Here are some guides to get you started with contributing:
node-cache is an in-memory key-value store similar to keyv but does not support multiple backends. It is purely for in-memory storage with TTL support.
levelup is a wrapper for LevelDB. It provides a key-value store with a rich set of features. Unlike keyv, levelup is more complex and is designed specifically for LevelDB.
ioredis is a robust, performance-focused Redis client for Node.js. While keyv supports Redis as one of its backends, ioredis is dedicated solely to Redis and offers more advanced features specific to Redis.
memcached is a Node.js client for the memcached server. It is similar to keyv in providing a key-value cache but is specific to the memcached protocol and server.
FAQs
Simple key-value storage with support for multiple backends
The npm package keyv receives a total of 133,184,102 weekly downloads. As such, keyv popularity was classified as popular.
We found that keyv demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.