Durable state
What the writer has acknowledged.
{"name":"Trail Backpack","price":79}DurableA small API has a lot happening underneath. Walk a request through Rockbee, move data between its views, and see exactly what shared object storage changes.
Start with a write01 / FOLLOW THE REQUEST
ILLUSTRATIVE WALKTHROUGH · NO LIVE CLUSTERPick an operation. Advance one step at a time to see who does the work, what gets stored, and when the client can call it done.
CLIENT
The table handle uses the same bounded client connection as the key-value API. The document is a JSON object.
let products = client.table("products");
products.put_key("product-42", &json!({
"name": "Trail Backpack", "price": 79
}), None).await?;
PUT success acknowledges SlateDB durability. Reader and SQL visibility follow separately.
Raw KV uses the same writer and reader routes. Only JSON table changes feed the Parquet projection.
The arrows follow real code: routing, durable writes, SQL execution.
02 / MAKE EVENTUAL CONSISTENCY VISIBLE
Start with a published product at $79. Change it, refresh the reader, then publish SQL. Try deleting it before both views catch up.
What the writer has acknowledged.
{"name":"Trail Backpack","price":79}DurableWhat products.get(id) can see.
{"name":"Trail Backpack","price":79}Caught upWhat the latest manifest exposes.
{"name":"Trail Backpack","price":79}PublishedThis is a model, with the catalogue already discovered. Buttons stand in for background work; they are not public client methods. GET and SQL can catch up in either order.
Delete changes logical visibility. Previously published Parquet files remain in object storage; automatic cleanup is not currently implemented.
03 / THE ARCHITECTURAL CHOICE
SlateDB keeps the durable database in object storage. Rockbee readers open that shared data and build their own working caches. SQL adds a separate Parquet representation.
SlateDB manages WAL files, sorted string tables, and metadata. The writer waits for durability before a successful mutation acknowledgement.
OPAQUE BYTES + JSON DOCUMENTSThe writer’s exporter turns a snapshot and later WAL changes into immutable Parquet. Manifests tell readers which files form a published table.
PARQUET SEGMENTS + MANIFESTSEach process has SlateDB caches. Each SQL reader also caches Parquet in local temporary files. Replacing a reader loses warmth, not authoritative data.
MEMORY CACHE + LOCAL QUERY FILESExtra readers add serving resources and cache capacity. Capacity gains depend on the workload and shared bottlenecks.
Reader capacity can grow without provisioning a complete durable database copy per reader. You still pay for compute, caches, object requests, transfers, and cold-start work. Operators provision the readers; the system does not automatically scale them.
04 / WHERE OTHER DESIGNS GET EXPENSIVE
Sometimes it is RAM. Sometimes it is replica storage, rebalancing, or operational work. The useful question is which tradeoff your application can afford.
Self-managed Redis Open Source with RDB / AOF persistence. Redis Cloud and Redis Software have different operating models.
RAM is a great place for a hot working set. Keeping an entire growing dataset there changes the economics.
Durable data lives in shared object storage; readers cache their working sets. The cost is eventual reader visibility and cold object fetches. Rockbee does not provide Redis’s data structures or promise its in-memory latency.
Self-managed Couchbase Server, including Enterprise Magma. Capella and other managed Couchbase offerings are separate comparisons.
A broad distributed database gives you more capabilities—and more service placement and capacity decisions.
A smaller topology: one writer, independent readers, and shared durable objects. You give up Couchbase’s broader indexing and service ecosystem. Rockbee’s SELECT-over-Parquet path is a different query contract.
PostgreSQL 18 with conventional physical primary / standby replication. Managed services and shared-storage variants can differ.
Read replicas bring more serving capacity. They also bring database copies, WAL replay, and another recovery path.
synchronous_commit = remote_apply waits until changes are applied and visible there.Readers use shared durable objects instead of maintaining a complete durable replica. Rockbee trades away multi-key transactions, immediate reader freshness, and automatic writer failover. A transactional system of record may still belong in PostgreSQL.
Choose it when byte-key lookups, flexible JSON, and an eventually published SQL view fit the workload. A single writer, eventual reads, and bounded single-table queries are part of that choice. Checkout transactions and strict read-after-write requirements call for a different contract.
Architecture comparison, reviewed 18 October 2026. Product scopes and official documentation are linked above. These are qualitative tradeoffs, not a performance benchmark or a price comparison.
05 / UNDER THE HOOD
The API, protocol, storage layout, publication rules, and failure paths. Implementation links are pinned to the revision reviewed for this guide.
RockBeeClient exposes put, get, and delete for nonempty opaque byte keys and values. User keys are limited to 65,534 bytes; the complete wire frame must also fit its 1 MiB bound. client.table("products") creates a JSON-table handle sharing the same connection, admission limits, and shutdown lifecycle. It uses composition, not inheritance.
use rockbee_client::RockBeeClient;
use serde_json::json;
let client = RockBeeClient::connect("127.0.0.1:7100").await?;
// Byte values: the application chooses the encoding.
client.put(b"cart:customer-42", b"product-42:2", None).await?;
let cart = client.get(b"cart:customer-42").await?;
// JSON table: generate a UUID, or choose your own ID.
let products = client.table("products");
let id = products.put(&json!({
"name": "Trail Backpack", "price": 79
})).await?;
let product = products.get(&id).await?;
// Either GET can lag a preceding durable PUT.put_key(id, document, ttl) replaces a document under a supplied ID. JSON objects can have different fields. Raw keys and table IDs live in separate physical namespaces.
A PUT starts eagerly and returns a completion handle. Dropping an admitted handle does not cancel the write. Awaiting success confirms that the writer acknowledged SlateDB durability. Shutdown drains admitted operations, including dropped handles, before disconnecting.
| Outcome | What the application knows |
|---|---|
NotSent | The client did not enqueue the request. |
Rejected | A server response confirms the mutation was not applied. |
Unknown | The mutation may have become durable. A lost reply cannot settle that question. |
The client and gateway do not automatically replay uncertain mutations. Request IDs correlate responses on a connection; they are not durable deduplication keys. By default, client clones share 128 active and 128 pending slots, with a 5-second end-to-end request deadline.
proto/rockbee.proto defines the versioned envelope, requests, responses, and correlation IDs. Prost generates Rust types; the generated file is checked into the repository and included by the protocol crate. Ordinary Cargo builds use that file. Regeneration is an explicit developer command.
4-byte big-endian length | Protobuf Envelope
version = 1
request_id = positive integer
oneof: KV / Table / Status / Response
Maximum frame: 1 MiB, including the length prefix.This is a custom TCP protocol. Multiplexed requests use positive IDs; zero is reserved for sequential mode. The shared RPC layer bounds connections and in-flight work. Its frame read/write timeouts are separate from client and query execution deadlines.
cargo run -p rockbee-protocol --example generate_protocolValidated mutations take the one writer route exactly once. GET and SQL requests go to readers in round-robin order, with reader failover inside one routing budget. A successful missing-value response is final; the gateway does not ask another reader to search for a newer value.
Readers use SlateDB’s read-only DbReader, replay WAL data, and refresh their view. The checked-in poll interval is 10 seconds. That is a polling setting, not a visibility guarantee. Lower intervals trade extra object-store requests for more frequent refresh.
The gateway has no separate KV cache. Each writer or reader has SlateDB block and metadata caches; SQL adds a reader-local Parquet file cache. Cold requests can fetch durable objects, while warm requests may use the working set already present.
Rockbee keeps opaque byte keys and JSON tables in the same SlateDB database, under disjoint physical key prefixes. A table name is an ASCII identifier of 1–64 bytes; a document ID is UTF-8 and may be up to 1,024 bytes. A table value must be a JSON object. A table PUT writes the document and a durable catalogue entry in one SlateDB batch, so the exporter can discover the table after a restart.
raw key = [0, ...]
table prefix = [1, table_name_length, ...table_name_bytes]
catalogue key = [2, ...table_name_bytes]
Export objects are deliberately outside the SlateDB data paths. The table name is hex encoded, preserving distinct case-sensitive names even when the object store is a local filesystem.
<prefix>/_rockbee_tables/<hex-table-name>/
segments/<uuid>.parquet
manifests/<20-digit-revision>.json
table limits · table key encoding · durable table PUT · object layout
A writer-owned background worker checks registered tables every 500 ms. A table’s first export, a compaction, or a detected WAL gap builds a new base generation. This work is independent of the mutation acknowledgement: the client receives success only after SlateDB durability, not after Parquet publication.
| Export bound | Value |
|---|---|
| Batch before encoding | 1,000 rows or approximately 8 MiB |
| Encoded Parquet segment | 32 MiB maximum |
| Snapshot checkpoint lease | 24 hours |
The manifest cursor is exported_wal_file_id. It advances only after a whole WAL file has reached EOF; uploaded partial batches are not made visible on their own. A missing/truncated WAL causes a fresh pinned snapshot rather than skipping history.
export constants · worker lifecycle · snapshot and tail reconciliation · WAL cursor rule
Each Parquet row contains the document ID and JSON value plus the ordering and visibility metadata needed to reconstruct current table state: seq, absolute expiry, tombstone, WAL file ID, and row ordinal. Segment writes use create-only object-store operations. Only afterwards can the worker publish a new create-only manifest revision.
{
"format_version": 1,
"revision": 42,
"writer_epoch": 9,
"exported_wal_file_id": 1234,
"segments": [{ "path": "…/segments/uuid.parquet", "bytes": 1048576 }]
}
Publication checks the active SlateDB writer epoch, requires the expected preceding revision, and rejects a cursor that would regress. The result is a complete immutable generation or the preceding published generation; a failed upload or publication does not make partial state queryable.
manifest schema · segment upload and publication · writer-epoch fence
A reader reloads the table manifest for every SQL query. It checks that every referenced segment stays inside that table’s object prefix, downloads any cache miss into a reader-owned temporary directory, verifies its manifest size, and gives DuckDB those local file paths. DuckDB does not directly read S3 or SlateDB for a query.
Before your SQL predicates run, Rockbee builds the selected table view by selecting the newest row for each id. The ordering is exact:
row_number() OVER (
PARTITION BY id
ORDER BY seq DESC, wal_file_id DESC, row_ordinal DESC
)
Only rank 1 remains; then its tombstone and expiry are evaluated. This ordering matters: an older live value cannot reappear when the newest record is a delete or has expired.
manifest reload and execution · local download cache · deduplication and visibility view
A table delete is a durable SlateDB tombstone. The exporter preserves it as a Parquet row with tombstone: true; queries remove it only after deciding that it is the newest row for that ID. This keeps a historical pre-delete value from resurfacing.
TTL is stored as an absolute Unix-millisecond expiry. ttl_millis = 0 means no expiry. At the exact expiry instant a point read is invisible, and query SQL uses the equivalent condition expires_at_unix_ms > now. A later non-expiring PUT replaces an earlier expiry because it is the newest record.
ttl_millis == 0 // Ttl::NoExpiry
ttl_millis > 0 // Ttl::ExpireAtMillis(now + ttl_millis)
table TTL and delete durability · wall-clock TTL semantics · tombstone export
Each query gets an in-memory DuckDB connection and a private spill directory. External access, persistent secrets, and extension auto-loading are disabled. SQL must be one SELECT; DuckDB parses it into a syntax tree, and Rockbee permits only the selected unqualified table and lexical CTEs. Expressions, joins over CTEs, and aggregates still run in DuckDB. Allowed table functions are range, generate_series, unnest, json_each, and json_tree; file scans, other tables, and catalogue access are rejected.
| Per reader / per query | Default bound |
|---|---|
| SQL text | 64 KiB |
| Concurrent SQL workers | 2; excess returns busy |
| Whole deadline, including downloads | 4 seconds |
| DuckDB memory / spill | 256 MB / 1 GiB |
| Result | 1,000 rows and 512 KiB JSON |
| Local Parquet cache | 8 GiB, 16,384 files, 32 MiB/object |
The reader configuration can change timeout, DuckDB memory (1–16,384 MB), and concurrency (1–128). It does not make query state durable or turn the cache into an authority.
reader defaults and configurable limits · DuckDB isolation and result bounds · SQL source restrictions
On writer restart, Rockbee detects registered tables and restarts export even without another mutation. The exporter retries transient object-store failures, but a newer SlateDB writer epoch fences the old exporter. On normal shutdown it cancels and joins the exporter before closing the database; owned checkpoint cleanup is attempted on completion, failure, and cancellation.
Compaction creates a new snapshot generation after at least 128 delta segments and 60 seconds. It replaces what the current manifest references, preserving the same visibility rules. Manifest revisions and unreferenced Parquet objects are currently retained; automatic garbage collection is not implemented.
SQL is eventually consistent with durable writes. A registered table with no first manifest returns a warming error. A lagging reader may not yet see the catalogue entry and can treat the table as empty. The returned exported_wal_file_id describes the export generation used; it is not a write receipt or a cross-reader read barrier.
restart resumes export · writer shutdown · compaction and WAL-gap recovery · warming behavior
The selected table exposes id VARCHAR and value JSON. Your SELECT shapes each result row. SQL reads the published projection, so this example result applies after the product is visible in that generation.
let result = products.query(
"SELECT id, value->>'name' AS name,
TRY_CAST(value->>'price' AS DOUBLE) AS price
FROM products
WHERE TRY_CAST(value->>'price' AS DOUBLE) < 100
ORDER BY price"
).await?;
for row in result.rows {
println!("{row}");
}{"id":"product-42","name":"Trail Backpack","price":79.0}The client returns JSON row values plus exported_wal_file_id. Results are bounded and materialized; this API does not return a streaming database cursor. The watermark describes the exported view, not confirmation that a particular recent write is present.
Writer and reader processes share the same backend, bucket or directory, and prefix. Current backend constructors cover S3, LocalStack, and local files. Native GCS and Azure backend configuration is not implemented. The gateway is configured with one writer and a list of readers.
# Reader-side SQL configuration
[query]
timeout_ms = 4000
memory_limit_mb = 256
max_concurrent = 2
[reader]
poll_interval_ms = 10000| Checked-in setting | Value |
|---|---|
| RPC connections / global in-flight | 256 / 128 per process |
| RPC frame read / write timeout | 30 s / 5 s |
| SlateDB block / metadata cache | 512 MiB / 128 MiB per process |
| Reader replay memtable maximum | 64 MiB per memtable |
| Writer unflushed-data backpressure | 1 GiB |
Cache capacities are not preallocated memory or total process RSS ceilings. Query memory is a DuckDB setting per query; concurrent queries and other process work consume additional resources.
rockbee-cdc tails durable SlateDB WAL changes into JSON lines. It flushes output before advancing a locked, source-bound cursor. Crashes may repeat events, so consumers need at-least-once handling. TTL expiry does not emit a delete event; engine-private table and catalogue keys are filtered from this raw KV stream.
make local starts an owned writer, reader, and gateway on dynamic ports with a unique storage prefix. It prints connection details. Fixed development configs use gateway 7100, writer 7101, and reader 7102. Start the writer before the reader.
--check-config reports effective configuration without proving connectivity. A status probe reports endpoint role and local RPC counters; it does not prove storage health or reader freshness.
Reader request failover is implemented. Automatic writer failover and write sharding are not. The public transport is plain TCP without built-in authentication or TLS; deployments must supply the required network boundary. SQL path restrictions are not a tenant-authentication system.
| Module | Owns |
|---|---|
rockbee-client | Public Rust API, eager completion handles, admission lifecycle |
rockbee-protocol | Protobuf types, framing, validation, mutation certainty |
rockbee-rpc | Bounded TCP server and response handling |
rockbee-runtime | Backend construction, shared config and memory settings |
rockbee-query | Writer export, manifest publication, reader SQL |
rockbee-gateway / writer / reader | Separately runnable serving processes |
rockbee-cli / cdc | Local workflow, requests, probes, WAL consumption |
The repository contains durability, lost-acknowledgement, reader convergence, SQL update/delete/expiry, and publication recovery tests. Local fixtures establish those behaviors under their test conditions; they do not establish production throughput or linear scaling.
Storage behavior tests · Query tests · Gateway failure tests
NOW YOU KNOW THE PATH