Skip to content
Storage / KV

Storage

KV

Use Workers KV namespaces with OpenOrange, backed by a local Rivet Actor.

Status: Supported. Runs on one Rivet Actor for each namespace.

Configuration

OpenOrange reads the standard Wrangler KV binding. No extra setup is needed:

{
  "kv_namespaces": [{ "binding": "CACHE", "id": "<NAMESPACE_ID>" }]
}

Like wrangler dev, OpenOrange uses preview_id when it is set, then id, then the binding name. Bindings with the same ID share data.

How it works

Each namespace is one internal Durable Object, which runs as a Rivet Actor with SQLite storage. Data persists across orange dev restarts in the project’s .open-orange directory, next to Durable Object storage.

The binding implements the Workers KV API:

  • get() and getWithMetadata() with text, json, arrayBuffer, and stream types, and bulk reads with an array of keys.
  • put() with string, ArrayBuffer, ArrayBufferView, and ReadableStream values, plus expiration, expirationTtl, and metadata.
  • delete().
  • list() with prefix, limit, and cursor. Keys list in UTF-8 byte order.

Argument checks and error messages follow workerd and the local KV backend of wrangler dev. cacheTtl is validated and otherwise ignored.

OpenOrange does not copy Cloudflare’s KV quotas: 512-byte keys, 1,024-byte metadata, 25 MiB values, 100 keys per bulk read, 1,000 keys per list(), and TTLs of at least 60 seconds. A list() page still holds 1,000 keys by default. The limit that applies is Rivet’s: one write stores at most 128 MiB of changed SQLite pages, so a put() of a key, value, and metadata of 128 MiB or more fails at once with a RangeError that names that limit.

Conformance

An end-to-end orange dev test covers every value and result type, metadata, bulk reads, listing with pagination, error text, shared and separate namespace IDs, overlapping writes, and persistence across restarts. The same Worker returns identical results under wrangler dev. The upstream workerd KV tests use a mock KV server that the conformance runner does not configure.

Limits and deviations

  • KV is strongly consistent locally. Cloudflare KV is eventually consistent, so a read in another location can return an older value for up to 60 seconds.
  • Expired keys read as absent immediately at their expiration second.
  • put() buffers the whole value before it stores it.
  • In a test on Rivet 3.0.6, a 64 MiB value stored and read back, but a 100 MiB value made the Engine fence the namespace’s storage, and the put() did not return. That is a Rivet issue under one large write, not a limit.
  • cacheStatus is always null.
  • There is no orange kv command to manage keys from the CLI.

Throughput

Each namespace is one actor, and one actor is the single writer for its storage. Every get(), put(), delete(), and list() on a namespace goes through that actor, so one namespace’s throughput is bounded by what one actor can do. Cloudflare KV spreads reads across its edge and has no such per-namespace bound. To spread load today, use more than one namespace. Sharding a namespace across actors, or backing KV with a Rivet Service, may lift this limit later.

Compatibility

From the latest conformance run. Scores on the compatibility page; every binding on the status page.

KV

Drop-in kv_namespaces

Runs on Internal Rivet Actor per namespace.

Strongly consistent, so reads never see stale values. cacheStatus is always null.

Scales by Namespace. One actor per namespace, so one namespace does not fan out across actors. Split namespaces for more.

Recorded results

WorksKVEventually consistent, with a global edge cache.Strongly consistent. Reads never see stale values, and cacheStatus is always null. 1/1 pass
Results
  • example: kv Pass

In the feature matrix

Alternativekv_namespaces (Workers KV)Binds a Workers KV namespace: a global, eventually consistent key-value store with edge caching.Each namespace id is an Orange-internal SQLite Durable Object on Rivet Actors. Data persists under .open-orange/rivetkit across orange dev restarts. 0/1 pass · 7 files
Notes
Reads see writes at once. Cloudflare can take 60 seconds or more to show a write in other locations. Orange has no edge cache. The local id is preview_id, then id, then the binding name, like wrangler dev. Upstream kv-test fails 0 of 5 because the conformance runner drops workerd kvNamespace bindings, not because of Orange's KV.
Tests
  • api-tests--kv-test › getTest AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: | + actual - expected | Fail · Behavior differs
Source
src/config/load-wrangler.ts:797-802src/config/load-wrangler.ts:669-677packages/runtime-node/src/runner.ts:501-508packages/runtime-node/src/bindings/internal-objects.ts:12-21packages/runtime-node/src/bindings/kv/object.ts:6-10src/runtime/bindings.test.ts:175-286packages/cloudflare-conformance/src/upstream/run-file.ts:378-413
SupportedKV get()get(key) returns the value as text, or null when the key is absent.Same result. Key validation and error text follow workerd and Miniflare. 4 files
Notes
cacheTtl is validated (at least 30) and then ignored, because Orange has no cache. Expired keys read as null.
Source
packages/runtime-node/src/bindings/kv/namespace.ts:239-275packages/runtime-node/src/bindings/kv/object.ts:111-124src/runtime/bindings.test.ts:175-192src/runtime/bindings.test.ts:206-223
SupportedKV get() with a typeget(key, type) returns text, json, arrayBuffer, or stream values.Supports the same four types and rejects other types with workerd's error text. 3 files
SupportedKV getWithMetadata()Returns the value, its metadata, and cacheStatus.Returns value, metadata, and cacheStatus. 2 files
Notes
cacheStatus is always null, because Orange has no cache.
Source
packages/runtime-node/src/bindings/kv/namespace.ts:245-275src/runtime/bindings.test.ts:175-192
SupportedKV get() with an array of keysReturns a Map for up to 100 keys. Only text and json types are allowed.Same Map result, type rules, no Cloudflare key-count or size limit. 4 files
SupportedKV put() with expiration and metadataWrites a string, ArrayBuffer, ArrayBufferView, or stream with optional expiration, expirationTtl, and metadata.Accepts the same value types and options, without Cloudflare's key, value, metadata, or TTL limits. A put of 128 MiB or more fails with Rivet's commit limit. 5 files
SupportedKV delete()Deletes one key. Deleting an absent key succeeds.Deletes the row from the namespace's SQLite table. An absent key succeeds. 2 files
Notes
Key validation matches get() and put().
Not verified by a test
No repository test calls delete(). Verified by reading the code only.
Source
packages/runtime-node/src/bindings/kv/namespace.ts:359-364packages/runtime-node/src/bindings/kv/object.ts:138
SupportedKV list() with prefix, limit, and cursorLists keys in byte order with an opaque cursor, up to 1000 keys per page.Lists keys in UTF-8 byte order with prefix, limit, and cursor. Expired keys are left out. 3 files
Notes
The cursor is the base64url form of the last key. Code must treat it as opaque, as on Cloudflare.
Source
packages/runtime-node/src/bindings/kv/namespace.ts:366-386packages/runtime-node/src/bindings/kv/object.ts:187-220src/runtime/bindings.test.ts:193-204
N/AKV deleteBulk()Not deployed. Workerd adds deleteBulk() only with the experimental flag, for tests.KVNamespace has no deleteBulk() method. 0/1 pass · 1 file
Notes
kv-test deleteBulkTest fails because the runner drops the KV binding.
Tests
  • api-tests--kv-test › deleteBulkTest TypeError: env.KV.deleteBulk is not a function Fail · Behavior differs
Source
packages/runtime-node/src/bindings/kv/namespace.ts:232-387