Hold a page,
not the universe.

Regolith is an ACID, performance oriented, embedded key-value engine for edge systems. An LSM-tree in pure Rust, with a read path that takes no locks and a scan that streams instead of materializing.

cargo add regolith   ·   no C toolchain · no FFI · no async runtime


A complete engine, not a subset.

The LevelDB design done properly: a write-ahead log, a lock-free skip-list memtable, leveled SSTables, compaction on an ordinary OS thread. Small enough to learn in an afternoon, wide enough to build a database product on.

ACID transactions

Pessimistic and optimistic, across read-committed, snapshot and serializable.

MVCC snapshots

A snapshot captures a sequence number and ignores anything newer.

No locks on read

One transaction shared across threads, no mutex around it.

Automatic crash recovery

A valid prefix of the write history. No gaps, no half-applied batch.

Column families

Separate keyspaces, own options, one WAL, one atomic batch.

Backups and checkpoints

Full backups, and hardlinked checkpoints that cost almost nothing.

Merge operators

Fold an update in without reading the value first.

Compaction filters

Drop or rewrite entries as compaction passes over them.

Bulk SST ingestion

Build an SSTable outside the database and ingest it whole.

Time to live

Entries expire on their own, reclaimed during compaction.

Tailing iterators

Follow a keyspace as it changes.

Zero-copy reads

get_slice borrows the bytes the database already holds.

Bloom filters and block cache

A miss is refused before it touches disk.

Statistics and event hooks

Tickers, histograms, and callbacks on flush and compaction.

Rate limiting

A token bucket with priorities, so compaction cannot starve writes.

Three compaction styles

Level, FIFO and universal.

Runs in a browser

wasm32-wasip1, and wasm32-unknown-unknown through OPFS.

Runs on embedded systems

Options::embedded() targets a 1 to 4 MiB working set.

The scan that does not eat your RAM

The same range, read three ways. The only thing that changes is whether the scan is held in memory or streamed through.

peak memory, materialized against streamed

-0.00x

smaller peak. Streaming the same scan holds 792 KiB instead of 34.7 MiB.

Materialize the range 0.0 MiB

collect the whole scan before reading it

Stream it with a cursor 0 KiB

one entry held at a time

Page it, 100 entries deep 0 KiB

scan_page, explicit page-sized reads


Small surface, honest tradeoffs

Open a database, write a batch atomically, take a snapshot, walk a range. The parts that cost you something say so in the type.

a batch is all of it, or none of it
use regolith::{Db, Options, WriteBatch};

let db = Db::open("/tmp/my_db", Options::default())?;

db.put(b"hello", b"world")?;

let mut batch = WriteBatch::new();
batch.put(b"a", b"1");
batch.put(b"b", b"2");
db.write(batch)?;

// A snapshot is a point in time.
let snap = db.snapshot();
db.put(b"a", b"changed")?;
assert_eq!(snap.get(b"a")?.as_deref(), Some(&b"1"[..]));
a writer that costs a fixed budget, not the stream
use regolith::StreamOptions;

let mut writer = db.streaming_writer(StreamOptions {
    max_buffered_bytes: 1 << 20,
    ..Default::default()
});

for (key, value) in huge_source {
    // takes the buffer, does not copy it
    writer.put_owned(&key, value)?;
}
let sequence = writer.finish()?;

// Peak is the budget plus one operation,
// whatever the stream's length. Each flush
// is atomic; the stream as a whole is not.

Where it runs

Pure Rust throughout. Compaction runs on an ordinary OS thread; where there are no threads, set max_background_compactions to 0 and it runs on the calling thread.

targetstatus
linux, macos (x86_64, aarch64)full
wasm32-wasip1full, through a preopened directory
wasm32-unknown-unknownfull, through OPFS with Options::wasm()
embedded linuxOptions::embedded(), 1 to 4 MiB working set