~/satyajit

Rivet 3.0 and WarpDrive: S3 as the only dependency, and what the open tree shows

mdjsonmcp

2026-10-27 · 15 min · agents · infrastructure · storage · sqlite · object-storage

Why read this

Solidtop 85%

Reads the open Depot tree, finds the S3 tier Enterprise-only and WarpDrive absent, and runs a seeded toy: 6,000 PUTs direct vs 188 folded, 300/300 recoveries.

  • Original analysis
  • Runs on a laptop CPU
  • Concrete numbers to act on

Developer tools & infraApache-2.0Practitioner teardown

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 58 of 100, ranked 423 of 624 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

On 26 October Rivet posted a preview of Rivet 3.0, "built on WarpDrive, a new storage engine with bottomless storage on S3 and the speed of an SSD." The thread links the GitHub repository and the changelog post. The claim that matters to me is structural: S3 is the only dependency, Rivet is one stateless binary, and S3 never sits on the write path.

I wanted to know what is checkable. So I cloned the repo, read the storage layer, and built a small model of the access pattern to see what the claim costs and buys. This page keeps three labels apart. Read means I read it in the tree. Reported means Rivet says it and I could not check it. Measured or simulated means I ran it, and the simulated numbers rest on latencies I assumed, which the widgets let you change.

rivet-dev/rivet@80ee8b8 · snapshot 2026-10-27
tracked files
11,027
license
Apache-2.0
branch
main
tests
660 files
source
39.7 MB
commit date
2026-10-26
source by language
JavaScript17.0 MB(4977)Rust12.4 MB(1444)TypeScript9.9 MB(2568)Swift207.5 kB(32)HTML173.8 kB(44)Go95.2 kB(20)Shell63.6 kB(61)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-27 at 80ee8b8 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

What the post claims

Every number below is Rivet's, from the changelog. None of it is mine.

That is a clear design, and an old one in shape. A replicated log gives you durability and latency. An object store gives you capacity and a cheap copy of everything. The log is folded into big immutable objects so the object store sees few, large requests. The interesting questions are in the folding and the reads, so those are what I modelled.

What is in the open tree

I cloned rivet-dev/rivet at commit 80ee8b8 (26 October 2026, 13:28 UTC), about ninety minutes before the post. engine/packages/depot is the SQLite page store: 34,500 lines of Rust in src, plus one test file per invariant. The design is written down in docs-internal/engine/depot.md.

Read. Depot stores SQLite pages in UniversalDB ("UDB"), and the doc says UDB is the durable source of truth. engine/packages/universaldb has Postgres and RocksDB drivers. The page is 4,096 bytes (PAGE_SIZE in conveyer/keys.rs) and a shard is 64 pages (SHARD_SIZE in depot-client-types), so a shard is 256 KiB.

A commit encodes its dirty pages as LTX DELTA chunks, writes a PIDX row per page saying which delta owns it, writes COMMITS and VTX rows, and updates META/head. It never publishes a shard and never deletes history. That is the write-ahead log half. The other half is a set of per-database workflows. One manager plus a hot and a reclaim companion run per database branch. The hot companion folds deltas into full 256 KiB shard images and stages them; the manager copies them to the reader-visible SHARD rows, advances a watermark and clears the page-index entries; the reclaimer deletes old deltas only after re-proving, inside the delete transaction, that replacement coverage exists.

Two details are worth the trip. The first is in conveyer/constants.rs: a commit's pages are cut into segments on shard boundaries, at most five shards (320 pages) per segment. The comment explains why. A shard whose pages were split across two segments could be folded from one of them and written as an image missing the other's newer pages. I turn that into an experiment below. The second is MAX_COMMIT_DIRTY_PAGES = 32,768, which is 128 MiB of 4 KiB pages. The file says it is a bound from FoundationDB's 10 MB transaction limit, which is a hint about which store the hot tier was written against.

Read, and not in the open tree. The same repository contains constants for an S3 tier. CMP_S3_UPLOAD_LIMIT_BYTES is 64 MiB per upload activity, CMP_S3_UPLOAD_MAX_OBJECTS is 256, CMP_S3_DELETE_MAX_OBJECTS is 100, and a retired cold object waits 500 ms before deletion. There are key builders for CMP/cold_shard/ and codecs for cold shard references. But docs-internal/engine/oss-to-ee-upgrade.md says it plainly: "The cold storage subsystem is Enterprise-only, but OSS keeps its row schema rather than deleting it." The depot crate's lib.rs declares no cold_tier module, yet tests/fold_index.rs imports depot::cold_tier::{ColdTier, FilesystemColdTier} and a DbColdCompactorWorkflow. Those tests cannot build from this tree. The FilesystemColdTier the FAQ alludes to ("Rivet can offload data to a filesystem") exists only as an import.

Absent. grep -ri warpdrive over the tree at this commit finds nothing. No code path writes a replicated local-disk log across three zones. At this commit the README still describes the product as "Postgres or FoundationDB for persistence and tiered storage to S3," and its benchmark footnote says the 12.3 ms cold start and the 72.4 KB figure were "Measured September 2026 on FoundationDB." The post's 6.9 ms cold start is a different number on a different engine.

I do not read this as a problem with the claim. A preview announced after a commit lands can ship the code in the next commit, and the Enterprise cold tier is evidently the ancestor of what the post describes. It means that today you can read the page layout, the compaction protocol and the safety proofs, and you cannot read the three-zone log, the majority-ack path, or the thing that makes S3 the only dependency. Everything in the next two sections is therefore my model of the pattern, not Rivet's code.

What the S3 half costs

I need object-store numbers to model anything. The AWS performance guide says that for requests under 512 KB, "median latencies are often in the tens of milliseconds range," and that the per-prefix limits are 3,500 write and 5,500 read requests per second. S3 Express One Zone is sold on "consistent single-digit millisecond request latency." Request pricing is what makes writes expensive. I used S3 Standard's list price in us-east-1 as I remember it, $0.005 per 1,000 PUTs and $0.0004 per 1,000 GETs, and I did not re-check it against a bill. These are the prices in the dollar columns below.

I did not find a primary source for a latency distribution, so the toy assumes one: a lognormal around a chosen median with p99 at 4.5 times the median, plus 256 KiB at 60 MB/s per stream for the transfer. I swept the median over 15 to 80 ms. Treat every millisecond figure from here on as an assumption propagated through code, not a measurement of S3.

The toy

The script is toy.py, about 150 lines of Python with numpy, seeded, and it runs in seconds on one core. It has three parts.

Workload. A 47 MiB database is 12,032 pages in 188 shards. Commits arrive at 100 per second for 60 seconds, 6,000 in all. Each dirties one plus a Poisson(2) number of pages, 3.01 on average. Shards are chosen by a Zipf(1) law, pages within a shard uniformly. That is a guess at an Actor with a hot working set. A real SQLite file clusters its writes differently, so the absolute counts below are for this workload only.

Four write policies.

  1. PUT per commit. Each commit uploads its delta as one object and acknowledges when the PUT returns.
  2. Rewrite shard per commit. Each commit uploads a whole 256 KiB image for every shard it touched.
  3. WAL, fold, one object per shard. Acknowledge from a modelled majority of three zones (each replica a lognormal around 0.45 ms, ack at the second fastest plus 0.15 ms for the local sync). Every W seconds, upload one image per dirty shard.
  4. WAL, fold, packed segment. Same, but all of a window's dirty shards go into one object.

The third is what Depot's S3 constants describe: a complete shard set per fold, up to 256 objects. The fourth is the alternative I would reach for when the request count is the cost, and it trades that for range GETs on read.

On the 40 ms PUT setting, writing straight to the object store acknowledged at 39.7 ms p50 and 177 ms p99, and cost 6,000 PUTs, which is $5.00 per million commits and 74.4 MB uploaded. Rewriting touched shards per commit made 17,066 PUTs and uploaded 4,474 MB for the same 74.4 MB of logical changes, a 60-fold write amplification and $14.22 per million commits. At 25 ms and 80 ms medians the direct PUT acknowledged at 25.8 ms and 81.3 ms p50 (p99 110.9 and 340.6 ms), and the WAL rows did not move.

With the log in front, the acknowledgement is decoupled from S3 and the bill depends on the fold window. A 1 s window made 5,713 PUTs ($4.76 per million) and uploaded 1,498 MB, because at 100 commits per second nearly every shard in the hot set is dirty every second and is rewritten whole. A 5 s window made 2,057 PUTs. A 30 s window made 376. A 60 s window made 188, one image for each of the 188 shards, $0.16 per million commits and 49.3 MB uploaded, less than the 74.4 MB the commits carried because overlapping writes to a page collapse into one image. Packing the 60 s window into a single object made 1 PUT.

Two things follow, both from this model only. First, the claim "far fewer S3 operations" is arithmetic: requests scale with dirty shards per window, not with commits, so the ratio is commit rate times window over shard count. Second, the window is a real dial. A longer one costs fewer PUTs, and it also lengthens the log you replay on recovery and the span an S3 outage can cover before local disks fill. Rivet's FAQ says reads and writes of recent data survive an S3 outage. In this model that holds for as long as the log has room, and the post does not say how large that is.

Does the fold keep the data right

Cost numbers are cheap to produce. Correctness is where a pattern like this fails. The toy has a second, real component that does not use the latency model: a page store on a local directory, with an append-only WAL of records checksummed with SHA-256, immutable shard-image files, and a manifest swapped in with an atomic rename. A trial applies 5 to 40 random commits to a few shards, folds now and then (and 20% of the time abandons a fold after some images are written but before the manifest swap), and sometimes crashes in the middle of a commit, leaving a torn record that was never acknowledged. Recovery loads the manifest's images, replays WAL records newer than its watermark, and stops at the first torn or failing checksum. The oracle is a dictionary of acknowledged writes.

In 300 of 300 trials, recovery equalled the oracle.

I then broke the one rule from constants.rs. In the second variant a fold writes a shard image containing only the pages changed since the last fold, replacing the old image instead of rewriting the whole shard. That is the "image missing the other's newer pages" case the comment describes, and recovery matched the oracle in 196 of 300 trials. I did not classify the 104 failures further. Shard-aligned, complete images are what let the reclaimer delete the old deltas safely, and the cost is the write amplification visible in the 1 s row above. This is a test of my toy's logic and not of Depot, but it is the same invariant the Depot comment gives, and it fails the way the comment says.

Waking from S3

The post's least flattering number is also the most useful: 689 ms at p50 to wake an Actor whose 47 MiB database is only in S3 and answer its first query, against about 5 ms when it is on SSD. I cannot say what the query read. The post does not say, and a first query rarely touches every page. So I asked a bounded version of the question: how long to fetch all 188 shards of a 47 MiB database, at several GET latencies and degrees of parallelism?

At a 25 ms median and one GET at a time, the full fetch takes 6.6 s. With 4 in flight it takes 1.7 s, with 16 it takes 467 ms (p99 575 ms), and with 64 it takes 192 ms. At a 40 ms median and 16 in flight it takes 704 ms, close to the reported 689 ms. I read that as a plausibility check on the order of magnitude and nothing more: many combinations of latency, parallelism and bytes read produce 689 ms, and a design with a persisted preload set would read far fewer than 188 shards. The unit of fetch is what the table in the widget shows. At 25 ms, 12,032 serial GETs of one 4 KiB page each would take 371 s. Even 16 in parallel would take 23 s. 188 serial GETs of one shard each take 6.7 s. Object stores reward large requests, which is why a page store on S3 groups pages into shards at all.

The repo has its own evidence of the same pressure. SQLITE_OPTIMIZATIONS.md records a baseline of a 50 MiB cold full scan taking 20.14 s end to end, with 1,249 get_pages calls and 19.33 s spent in VFS transport, which is about 15.5 ms per call. Bounded speculative read-ahead of 64 pages cut the calls from 1,249 to 368. That document is a tracker that points to local notes, and it gives no latency for the 368-call case, so I do not know what the improvement was. Those calls also go to the hot tier in the open tree, not S3, so I would not subtract 20.14 s from 689 ms and call it a result. It does show the team measured a serial page-at-a-time cold read, saw it was too slow, and has been working on batching and preload hints (OpenConfig.preload_pgnos, persisted hot-page hints) ever since. The post's "pages it needs are already loaded before your code runs" is that preload story, and it is the part of the design I would most like to read.

What I could not check

What I take from it

The structure is sound and old. A small replicated log gives you the latency and the durability, and the object store gives you capacity. The log is folded into large, complete, immutable objects so that requests scale with dirty shards and not with commits. In my toy that ratio was 188 PUTs against 6,000 for the same 6,000 commits. The invariant that does the work is the one Depot's constants.rs states: fold whole shards, never partial ones, and only delete what a complete image covers. Break it in my toy and 104 of 300 trials end with the wrong data.

If you run Rivet today, the open tree gives you a UDB-backed page store with a full compaction protocol. The S3 tier is Enterprise code, and WarpDrive is a claim until its code lands. I would read the three-zone log first when it does, then the preload path that decides the cold wake, and then check the 689 ms against a database you can tell the shape of. The script that produced every simulated number here is one file; change the three latency assumptions at the top and rerun it.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Rivet 3.0 and WarpDrive: S3 as the only dependency, and what the open tree shows", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026rivetwarpdrive,
  author = {Satyajit Ghana},
  title  = {Rivet 3.0 and WarpDrive: S3 as the only dependency, and what the open tree shows},
  url    = {https://ai.thesatyajit.com/articles/rivet-warpdrive},
  year   = {2026}
}
share