fjall: a log-structured key-value engine that makes you pick your own transaction semantics
đź—» Log-structured, embeddable key-value storage engine written in Rust
At a glance
- What is it?
- An embeddable LSM-tree store in Rust with a BTreeMap-shaped API, keyspaces like column families, and a durability dial you set per write. The notable decision is that the default mode cannot do read-modify-write safely, and the README says so outright.
- Who is it for?
- fjall fits the case where you want RocksDB-shaped storage inside a Rust process without running a database server, the data is genuinely key-value, and you are willing to choose a durability level per write. Version 3.1.10 ships under MIT OR Apache-2.0 with a declared rust-version of 1.90.0, and releases 3.1.8 through 3.1.10 are mostly lsm-tree dependency moves plus targeted fixes, so the cadence is steady rather than eventful.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The BTreeMap-shaped API over an LSM-tree
Fjall, which the README says means Mountain in Nordic, is a log-structured embeddable key-value storage engine written in Rust. Two lines of the feature list do the most work: a thread-safe BTreeMap-like API, and 100% safe and stable Rust.
Underneath sits an LSM-tree, and the README compares the storage layer directly to RocksDB. That is the honest framing, because the operational model is the same: a journal, in-memory write buffers, background compaction, and a block cache. What Fjall changes is the surface you program against, which is a map rather than a handle and an iterator pair.
The README is equally upfront about two things it is not. It is not a standalone database server, so there is no wire protocol for a separate client to speak. And it is not relational or wide-column: it has no built-in notion of columns and no query language. If your data needs a secondary index built across fields, or any kind of join, that is outside this engine.
The rest of the list covers range and prefix searching with forward and reverse iteration, multiple keyspaces with cross-keyspace atomic semantics, built-in compression defaulting to LZ4, optional serializable transactions, optional key-value separation for large blobs, custom compaction filters, and automatic background maintenance.
Opening a database and creating your first keyspace
The install is a single cargo add, and the API you need first is small: open a database at a path, create a keyspace, then insert, get, remove and iterate.
cargo add fjalluse fjall::{Database, KeyspaceCreateOptions, PersistMode};
fn main() -> fjall::Result<()> {
let db = Database::builder(path).open()?;
let items = db.keyspace("my_items", KeyspaceCreateOptions::default)?;
items.insert("a", "hello")?;
let bytes = items.get("a")?;
items.remove("a")?;
db.persist(PersistMode::SyncAll)
}Two details in that sketch decide real behaviour. The README's comment on keyspaces says each one is its own physical LSM-tree and thus isolated from other keyspaces, which makes isolation structural rather than something you apply as a filter. And the Database::builder call has a TxDatabase::builder counterpart named in a comment, which is the fork in the road covered further down.
Searching is where the map shape pays off, because prefix and range iteration are first-class rather than bolted on:
for kv in items.range("a"..="z") {
// ...
}The README notes that iterators implement DoubleEndedIterator, so a prefix scan can be reversed with .rev() without collecting into a vector first. Keys are stored in lexicographic order, and for integer keys the README recommends the big endian form so ordering is predictable.
Durability is a parameter you pass, not a promise the engine makes
This is where Fjall differs most from what a database user expects, and it is stated plainly. After a write, you can choose to call Database::persist, which takes a PersistMode parameter. By default any operation flushes to OS buffers, but not to disk. The README says this matches RocksDB's default durability, which is an admission that the default can lose recent writes on a machine crash.
So there are two levels to reason about. Calling persist explicitly controls when the journal reaches the platter. Separately, dropping the database tries to persist the journal to disk synchronously, so an orderly shutdown is durable by default.
The consequence for an application is that a bare insert followed by a crash can lose the write, and nothing in the API signals that unless you asked for it. That is a reasonable trade for a throughput-oriented store and a poor default for an accounting ledger, which is why PersistMode is a per-call decision rather than a global setting chosen once at startup.
The default Database cannot do read-modify-write safely
The transactional section is the most important part of the README and is unusually candid for a project document. The backing lsm-tree store is MVCC and allows repeatable snapshot reads. But that isolation level cannot perform read-modify-write operations without a chance of lost updates, and WriteBatch does not let you read intermediary state back the way a proper transaction would.
The conclusion the README draws is that if you need transactional semantics you must use one of the transactional implementations, named as OptimisticTxDatabase or SingleWriterTxDatabase. The summary that follows says Fjall supports both transactional and non-transactional workloads, and that you probably want a transactional database unless you know your workload does not need one.
The naming is informative too: OptimisticTxDatabase for concurrent writers that may retry, SingleWriterTxDatabase when a single writer avoids the retry path entirely.
The practical lesson is that the API you meet first, in the basic usage section, is the non-transactional one. Choosing well means reading the transactional section before writing code and picking the database type deliberately, rather than starting from Database::builder and discovering the constraint later.
Why holding a Slice can keep a whole block alive
Memory management follows from the LSM design, and one consequence is easy to trip over. Memory for loaded data and indexes is managed per block and capped by the block cache capacity. That applies to returned values as well: when you hold a Slice, it keeps the backing buffer alive, and that buffer may be an entire block.
The README's advice is to copy the value out if you intend to keep it for a long time, into a Vec<u8>, Box<[u8]>, Arc<[u8]> or a fresh Slice made with Slice::new. This is the kind of note that saves a debugging session later, because the symptom of getting it wrong is a block cache that never evicts what you believed was a small value.
There are two independent memory structures to keep straight. The block cache holds the on-disk index and data. Orthogonally to it, each Keyspace has its own write buffer, described as the memtable, which is the unit of data flushed back into the index structure. Configuring max_memtable_size on KeyspaceCreateOptions sets the flush threshold.
The README suggests configuring the block cache to roughly 20 to 25 percent of available memory, or more when the data set fits fully into memory.
Thread safe yes, multiprocess no, and let it crash
Concurrency has one clear rule and one clear exception. Fjall is internally synchronized for multi-threaded access, so Database and Keyspace can be cloned without external locking, and there is a tokio example in the examples directory for async use. The exception is stated as a warning: a single database may not be loaded in parallel from separate processes. Being a library also means there is no network service to scale out to.
Error handling goes against the instinct most application developers have. Fjall returns an error enum, but the README says those variants are mostly used for debugging and tracing, and that an application is not expected to handle specific errors. The recommendation is to let the application crash and restart, which the README ties to a linked paper on CuttleFS as the safest way to recover from transient I/O errors.
That is defensible for an embedded engine, since recovery is a restart rather than a repair path, but it does mean an unrecoverable error takes the process with it. Anyone needing graceful degradation or partial recovery has to build that above the engine.
For the build itself, Cargo.toml records version 3.1.10, edition 2021, rust-version 1.90.0, and MIT OR Apache-2.0. The default feature set is lz4 alone, with optional bytes_1, metrics, and an internal __internal_whitebox feature that the package metadata keeps on a denylist. The lsm-tree dependency is versioned in step with Fjall itself, and releases 3.1.8, 3.1.9 and 3.1.10 are largely moves of that dependency plus fixes for numbered issues. The last push was on 2026-09-27 and the repository is not archived.
Editorial conclusion
fjall fits the case where you want RocksDB-shaped storage inside a Rust process without running a database server, the data is genuinely key-value, and you are willing to choose a durability level per write. Version 3.1.10 ships under MIT OR Apache-2.0 with a declared rust-version of 1.90.0, and releases 3.1.8 through 3.1.10 are mostly lsm-tree dependency moves plus targeted fixes, so the cadence is steady rather than eventful. The choice to make before anything else is transactional or not: the default Database gives a thread-safe map with snapshot reads but no safe read-modify-write, and the README's own summary says you probably want a transactional implementation. Start with cargo add fjall, then read the Durability section before writing the first insert.
Frequently asked questions
Is fjall a replacement for RocksDB?
It is the same storage model wrapped in a Rust-native API rather than a C++ library with a C API. The README states that fjall is 100% safe and stable Rust and that its LSM-tree storage is similar to RocksDB, including matching default durability behaviour. What you give up is RocksDB's cross-language clients and its standalone server mode.
How do I make writes durable in fjall?
Call Database::persist with a PersistMode parameter after writing, since by default an operation only flushes to OS buffers and not to disk. Dropping the database also attempts a synchronous persist of the journal, so an orderly shutdown is durable without an explicit call.
Can fjall run transactions?
Only through the transactional implementations, OptimisticTxDatabase or SingleWriterTxDatabase. The default Database sits on an MVCC store that gives repeatable snapshot reads but cannot do read-modify-write without a chance of lost updates, and WriteBatch does not return intermediary state.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fjall-rs-fjall)