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()andgetWithMetadata()withtext,json,arrayBuffer, andstreamtypes, and bulk reads with an array of keys.put()with string,ArrayBuffer,ArrayBufferView, andReadableStreamvalues, plusexpiration,expirationTtl, andmetadata.delete().list()withprefix,limit, andcursor. 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. cacheStatusis alwaysnull.- There is no
orange kvcommand 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
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
- Notes
- A stream value is the whole value buffered in one chunk.
- Source
- packages/runtime-node/src/bindings/kv/namespace.ts:97-122src/runtime/bindings.test.ts:175-192src/runtime/fixtures/bindings/worker.js:40-51
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
- Notes
- getWithMetadata() with an array returns value and metadata per key.
- Source
- packages/runtime-node/src/bindings/kv/namespace.ts:277-337packages/runtime-node/src/bindings/kv/object.ts:156-171src/runtime/bindings.test.ts:175-192src/runtime/bindings.test.ts:206-223
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
- Notes
- A stream value is read fully into memory before the write. Expired keys are removed when they are next read.
- Source
- packages/runtime-node/src/bindings/kv/namespace.ts:138-223packages/runtime-node/src/bindings/kv/namespace.ts:339-357packages/runtime-node/src/bindings/kv/object.ts:177-185src/runtime/bindings.test.ts:175-192src/runtime/bindings.test.ts:206-223
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