Cloudflare Workers KV storage adapter for Keyv
Use Cloudflare Workers KV as a Keyv storage backend in one of two modes:
bind(default) — a native KV binding, the object exposed asenv.MY_KVinside a Cloudflare Worker (or a Miniflare namespace in tests). Use this when your code runs inside the Workers runtime.rest— the Cloudflare REST API, authenticated with account credentials. Use this from any plain Node.js process (a server, a job, a CLI) that can't get a Worker binding.
Features
- Two transport modes —
bind(native binding, default) andrest(REST API) — selectable via themodeoption - Millisecond-precise TTLs enforced client-side, with a native KV
expirationTtlon every expiring key so Cloudflare reclaims space on its own - Namespace support for key isolation across multiple Keyv instances
setMany,getMany,deleteMany, andhasManybatch operations- Async
iteratorsupport with namespace-aware filtering and automatic pagination - Fully testable locally with Miniflare — no Cloudflare account required
createKeyvhelper for quick setup
Note: Cloudflare KV is eventually consistent. It caches reads (for 60 seconds by default), so a read after a write or delete can return the previous value until that cache expires. Changes are usually visible straight away in the location that made them, but not always: over the REST API, a
get()straight afterdelete()can still return the deleted value. KV's native expiry also has a 60-second minimum. This adapter stores an expiry timestamp alongside each value and enforces it on read, so TTLs behave precisely regardless of the native minimum. See the Cloudflare KV docs for details.
Table of Contents
- Install
- Quick Start with createKeyv
- Usage with a Worker Binding
- Usage with the REST API
- Usage with Namespaces
- Testing Locally with Miniflare
- How Values Are Stored
- Options
- Properties
- Methods
- License
Install
npm install --save keyv @keyv/cloudflare-kv
Quick Start with createKeyv
import { createKeyv } from '@keyv/cloudflare-kv';
// REST mode (from any Node.js process)
const keyv = createKeyv({
mode: 'rest',
accountId: process.env.CF_ACCOUNT_ID,
namespaceId: process.env.CF_KV_NAMESPACE_ID,
apiToken: process.env.CF_API_TOKEN,
});
// set a value
await keyv.set('foo', 'bar');
// get a value
const value = await keyv.get('foo');
// set with TTL (milliseconds)
await keyv.set('foo', 'bar', 60000);
// delete a value
await keyv.delete('foo');
Usage with a Worker Binding
Inside a Cloudflare Worker, pass the binding (e.g. env.MY_KV) directly:
import Keyv from 'keyv';
import KeyvCloudflareKV from '@keyv/cloudflare-kv';
export default {
async fetch(request, env) {
// `bind` is the default mode; passing a binding is all you need.
const store = new KeyvCloudflareKV({ kvNamespace: env.MY_KV });
const keyv = new Keyv(store);
await keyv.set('foo', 'bar');
return new Response(await keyv.get('foo'));
},
};
You can also pass the binding directly as the only argument:
const store = new KeyvCloudflareKV(env.MY_KV);
Usage with the REST API
From a regular Node.js server, set mode: 'rest' and authenticate with a Cloudflare API token that
has the Workers KV Storage Edit permission:
import Keyv from 'keyv';
import KeyvCloudflareKV from '@keyv/cloudflare-kv';
const store = new KeyvCloudflareKV({
mode: 'rest',
accountId: 'your-account-id',
namespaceId: 'your-kv-namespace-id',
apiToken: 'your-api-token',
});
const keyv = new Keyv(store);
await keyv.set('foo', 'bar');
const value = await keyv.get('foo'); // 'bar'
modedefaults to'bind'. If you omit it, the adapter infers'rest'when you pass REST credentials and'bind'when you pass a binding — but setting it explicitly is recommended.
Usage with Namespaces
import Keyv from 'keyv';
import KeyvCloudflareKV from '@keyv/cloudflare-kv';
// Keyv passes its namespace to the adapter, which stores `foo` as `namespace1:foo`.
// Give each Keyv instance its own adapter.
const store1 = new KeyvCloudflareKV({ kvNamespace });
const keyv1 = new Keyv(store1, { namespace: 'namespace1' });
const store2 = new KeyvCloudflareKV({ kvNamespace });
const keyv2 = new Keyv(store2, { namespace: 'namespace2' });
// keys are isolated by namespace
await keyv1.set('foo', 'bar1');
await keyv2.set('foo', 'bar2');
const value1 = await keyv1.get('foo'); // 'bar1'
const value2 = await keyv2.get('foo'); // 'bar2'
Testing Locally with Miniflare
The adapter accepts any object implementing the Cloudflare KVNamespace interface, so you can
test against a real local KV — no account needed — using
Miniflare:
import { Miniflare } from 'miniflare';
import KeyvCloudflareKV from '@keyv/cloudflare-kv';
const mf = new Miniflare({
modules: true,
script: "export default { fetch() { return new Response('ok'); } };",
kvNamespaces: ['KV'],
});
const kvNamespace = await mf.getKVNamespace('KV');
const store = new KeyvCloudflareKV({ kvNamespace });
await store.set('foo', 'bar');
console.log(await store.get('foo')); // 'bar'
await mf.dispose();
How this package is tested
The test suite runs against a real local Cloudflare KV, not in-memory stubs:
bindmode is tested directly against a Miniflare KV namespace.restmode is tested end-to-end against a small local HTTP server that implements the Cloudflare KV REST API on top of that same Miniflare namespace — so the REST client exercises real local storage rather than mockedfetchresponses.
This local server emulates Cloudflare's live REST environment. We keep it in sync with the
Cloudflare API and additionally run a live integration test (the cloudflare-keyv-integration
GitHub workflow) against the real Cloudflare KV API, so we notice if the live behavior ever drifts
from the emulation. That workflow requires the CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_KV_NAMESPACE_ID,
and CLOUDFLARE_API_TOKEN_KV_TESTS repository secrets and self-skips when they are absent.
How Values Are Stored
Keyv owns serialization, so the adapter stores the already-serialized value string directly —
it does not wrap values in its own JSON envelope. The absolute expiry is stored in Cloudflare KV
metadata ({ e: <unix-ms> }) rather than mixed into the value.
Because KV's native expiry has a 60-second minimum, the adapter enforces expiry on every read using
that metadata, so TTLs are millisecond-precise. It also passes a native KV expirationTtl, so
Cloudflare removes the entry on its own, even one that is never read again. A TTL shorter than the
60-second minimum gets the minimum, so the entry stays in KV for up to about a minute after its
deadline, but reads already treat it as missing.
When used directly (not through Keyv), the adapter expects string values, since KV only stores strings. A non-string passed directly is coerced with
String(). Wrap the adapter in aKeyvinstance to store arbitrary values.
Options
For bind mode provide a kvNamespace binding; for rest mode provide accountId,
namespaceId, and apiToken.
| Option | Type | Default | Description |
|---|---|---|---|
mode |
'bind' | 'rest' |
'bind' |
Transport to use. Inferred from the other options when omitted. |
kvNamespace |
KVNamespace |
— | A Cloudflare KV binding (Worker binding or Miniflare namespace). Used by bind mode. |
accountId |
string |
— | Cloudflare account ID (rest mode). |
namespaceId |
string |
— | KV namespace ID — not the binding name (rest mode). |
apiToken |
string |
— | Cloudflare API token with Workers KV Edit permission (rest mode). |
url |
string |
https://api.cloudflare.com/client/v4 |
Override the REST base URL (rest mode). |
namespace |
string |
undefined |
Key prefix for namespace isolation. |
namespaceSeparator |
string |
'::' |
Separator placed between the namespace and key. |
Properties
.mode
The resolved transport mode in use. Read-only.
| Type | Default |
|---|---|
'bind' | 'rest' |
'bind' |
.client
The underlying KVNamespace (a binding or the built-in CloudflareKVRestClient). Useful for
direct, low-level access.
| Type | Default |
|---|---|
CloudflareKVNamespace |
Derived from the options |
.namespace
Key prefix for namespace isolation. When set, all keys are prefixed with namespace:.
| Type | Default |
|---|---|
string | undefined |
undefined |
.namespaceSeparator
The separator between the namespace and key.
| Type | Default |
|---|---|
string |
'::' |
Methods
constructor(options)
Creates a new KeyvCloudflareKV instance. Pass a KeyvCloudflareKVOptions object or a Cloudflare
KV binding directly.
import KeyvCloudflareKV from '@keyv/cloudflare-kv';
// Binding
const store = new KeyvCloudflareKV({ kvNamespace });
// REST credentials
const store2 = new KeyvCloudflareKV({ accountId, namespaceId, apiToken });
.get(key)
Retrieves a value. Returns the stored value or undefined if the key does not exist or has expired.
.getMany(keys)
Retrieves multiple values. Returns an array of values (undefined for missing/expired keys).
.set(key, value, expires?)
Stores a value with an optional absolute expires timestamp (Unix ms since epoch).
.setMany(entries)
Stores multiple { key, value, expires? } entries. Returns a boolean[] with per-entry success.
.delete(key)
Deletes a key. Returns true if the key existed, false otherwise.
.deleteMany(keys)
Deletes multiple keys. Returns a boolean[] indicating which keys existed.
.clear()
Clears the store. If a namespace is set, only keys with that prefix are deleted; otherwise all keys in the KV namespace are removed.
.has(key)
Returns true if a key exists and is not expired, false otherwise.
.hasMany(keys)
Returns a boolean[] indicating whether each key exists.
.iterator()
Returns an async iterator over [key, value] pairs. When a namespace is set, only namespaced keys
are yielded and the prefix is stripped from the returned keys. Pagination is handled automatically.
for await (const [key, value] of store.iterator()) {
console.log(key, value);
}
.disconnect()
No-op. Cloudflare KV uses stateless HTTP requests / in-process bindings, so there is no connection to close.
.formatKey(key) / .createKeyPrefix(key, namespace?) / .removeKeyPrefix(key, namespace?)
Namespace-aware key helpers, matching the other Keyv adapters.