cf-ioredis
v0.1.1
Published
ioredis-style client backed by Cloudflare KV over HTTP and WebSocket
Maintainers
Readme
cf-ioredis
cf-ioredis provides an ioredis-style API over a Cloudflare KV backend with both HTTP and WebSocket transports.
This is not a real Redis transport. It is a Redis-shaped client for the subset of operations that can map cleanly to a Worker-backed Cloudflare KV service.
Install
npm install cf-ioredisDeploy Worker
Use this button to deploy the companion Cloudflare Worker to your own account before pointing the client at its HTTP or WebSocket URL.
Configuration
Config precedence is:
- constructor URL
- constructor options
- environment variables
- defaults
Environment variables
CLOUDFLARE_KV_URLCLOUDFLARE_KV_TOKENCLOUDFLARE_KV_TIMEOUT_MSCLOUDFLARE_KV_KEY_PREFIXCLOUDFLARE_KV_TRANSPORTCLOUDFLARE_KV_WS_URL
URL format
Use cfkv:// or redis+cfkv://.
Example:
cfkv://[email protected]/kv?timeoutMs=5000&keyPrefix=app:The URL is converted to an HTTPS Worker base URL internally.
Usage
Read from env
import { Redis } from 'cf-ioredis'
const redis = new Redis()
const value = await redis.get('user:1')Override env with URL
import { Redis } from 'cf-ioredis'
const redis = new Redis('cfkv://[email protected]/kv?keyPrefix=demo:')
await redis.set('user:1', 'alice')Use options object
import { Redis } from 'cf-ioredis'
const redis = new Redis({
url: 'cfkv://[email protected]/kv',
timeoutMs: 3000,
keyPrefix: 'app:'
})Use WebSocket transport
import { Redis } from 'cf-ioredis'
const redis = new Redis({
url: 'cfkv://[email protected]/kv',
transport: 'ws',
wsUrl: 'wss://worker.example.com/ws',
allowEmulatedCommands: true
})Run the repo example
npm run build
npm run example:wsThis example lives in examples/node-websocket/ and shows the correct shutdown pattern for WebSocket transport with await redis.quit().
Pub/Sub
import { Redis } from 'cf-ioredis'
const publisher = new Redis({
url: 'cfkv://[email protected]/kv',
wsUrl: 'wss://worker.example.com/ws'
})
const subscriber = new Redis({
url: 'cfkv://[email protected]/kv',
wsUrl: 'wss://worker.example.com/ws'
})
subscriber.on('message', (channel, message) => {
console.log(channel, message)
})
await subscriber.subscribe('updates')
await publisher.publish('updates', 'hello')
await subscriber.unsubscribe('updates')
await Promise.all([publisher.quit(), subscriber.quit()])Pub/sub behavior:
subscribeandunsubscribeuse a dedicated WebSocket pub/sub connectionpublishprefers WebSocket when there is an active pub/sub socket for that channelpublishfalls back to HTTPPOST /publishwhen no active pub/sub socket is available- v1 supports exact channel names only, with live delivery only
Run the pub/sub example
npm run build
npm run example:pubsubThis example lives in examples/node-pubsub/.
Supported API
The current surface focuses on string and key operations.
| Method | Status | Caveat |
| --- | --- | --- |
| get | supported | returns string | null |
| set | supported | returns "OK" or null for rejected conditional writes |
| del | supported | integer reply |
| exists | supported | integer reply |
| mget | supported | ordered array of values |
| mset | supported | object-based input in v1 |
| expire | supported | seconds mapped to Worker ttl ms |
| pexpire | supported | millisecond ttl |
| ttl | supported | derived from Worker ms ttl |
| pttl | supported | raw ms ttl |
| persist | supported | removes ttl if Worker supports it |
| type | supported | returns string or none |
| pipeline | supported | local queued batch executor |
| multi | emulated | requires allowEmulatedCommands: true, not atomic |
| publish | supported | uses pub/sub WS when active, otherwise HTTP fallback |
| subscribe | supported | exact channel names only, requires wsUrl and WebSocket support |
| unsubscribe | supported | exact channel names only, requires wsUrl and WebSocket support |
| quit | supported | returns "OK" |
| disconnect | supported | no-op compatibility method |
Unsupported API Families
- hashes
- lists
- sets
- sorted sets
- streams
- scripting
- watch/unwatch
- cluster/sentinel/server commands
Unsupported methods should throw UnsupportedCommandError.
Pipeline semantics
pipeline() queues commands locally and executes them in order.
const result = await redis.pipeline().get('a').set('a', '2').del('a').exec()Result format matches common ioredis tuple style:
[
[null, '1'],
[null, 'OK'],
[null, 1]
]This is not Redis wire pipelining.
Transaction semantics
multi() is an emulated transaction-shaped wrapper on top of the same local queue.
- not atomic
- no optimistic locking
- no
watch - no rollback
Enable it explicitly:
const redis = new Redis({
url: 'cfkv://[email protected]/kv',
allowEmulatedCommands: true
})
const result = await redis.multi().set('a', '1').get('a').exec()Included Worker
The repo includes a first-party Cloudflare Worker backend under worker/.
worker/src/index.tsis the Worker entrypointworker/src/router.tsuseshonoto handle HTTP routes and auth middlewareworker/src/ws.tshandles WebSocket request/response messagesworker/src/kv.tsis the single source of truth for KV persistence and TTL metadata behavior
The Worker stores value payloads and TTL metadata in separate KV keys so ttl, pttl, and persist behave consistently across HTTP and WS.
Local Worker development
cd worker
npm install
npm test
npx wrangler devFor a public template/deploy-button flow, worker/wrangler.jsonc is set up for automatic KV provisioning. Configure AUTH_TOKEN as a Worker secret if you want bearer-token protection.
Worker backend contract
The library expects a Worker or HTTP service that exposes operations like:
GET /get?key=...POST /setPOST /mgetPOST /msetDELETE /deletePOST /existsPOST /expireGET /ttl?key=...POST /persistGET /type?key=...POST /publishGET /wsfor WebSocket upgradeGET /pubsub/ws?channel=...for pub/sub WebSocket upgrade
Payloads are JSON and values are encoded into a small envelope so future non-string types can be introduced without changing storage format.
WebSocket messages use the same action model as the transport layer:
{
"id": "1",
"action": "get",
"payload": {
"key": "user:1"
}
}Responses are correlated by id and return either data or a typed error payload.
Pub/sub uses a separate WebSocket protocol with frames like:
{ "type": "subscribe", "channels": ["updates"] }and message deliveries like:
{ "type": "message", "channel": "updates", "message": "hello" }Development
npm install
npm test
npm run build
npm --prefix worker install
npm --prefix worker testLocal End-To-End Tests
Run the real Worker locally with wrangler dev and exercise the real client over both HTTP and WebSocket:
npm run test:integration:localThis suite:
- starts the Worker from
worker/ - injects a local
AUTH_TOKEN=test - tests supported client methods over HTTP
- tests supported client methods over WebSocket
- prints warm local latency samples
To print only the local latency benchmark output:
npm run bench:localCurrent illustrative deployed measurement from the live workers.dev test run:
- warm HTTP
getaverage: about189ms - warm WebSocket
getaverage: about112ms
Treat these as directional numbers only; latency depends on region, Cloudflare account state, network path, and whether the test is local or deployed.
