# heed: typed LMDB bindings for Rust, in two flavours

> heed wraps the Lightning Memory-Mapped Database in Rust with typed key and value codecs so that a database is declared as a pair of types rather than as raw byte buffers. It ships in two variants, heed and heed3, tracking the two LMDB branches, one of which adds encryption at rest.

**meilisearch/heed** — Fully typed LMDB wrappers with minimum overhead 🐦

- Repository: https://github.com/meilisearch/heed
- Website: https://docs.rs/heed
- Stars: 921 · Forks: 86
- Language: Rust
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/meilisearch-heed

## A key-value store declared as two types

heed is described as Rust-centric LMDB abstractions with minimal overhead, a set of libraries that enable the storage of various Rust types within LMDB and extend that support to Serde-compatible types.

The design decision that makes it pleasant to use is that a database is typed at the type level. In the example, the database is declared as a pair of codecs:

```rust
use heed::{EnvOpenOptions, Database, byteorder};
use heed::types::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let env = unsafe { EnvOpenOptions::new().open("my-first-db")? };

    // We open the default unnamed database
    let mut wtxn = env.write_txn()?;
    let db: Database<Str, U32<byteorder::NativeEndian>> = env.create_database(&mut wtxn, None)?;
```

That is the whole abstraction. Str is the key type, so keys are strings, and U32 with the native byte order is the value type, so values are 32-bit unsigned integers in the machine's own byte order. The put and get calls that follow then take those Rust types directly:

```rust
    // We open a write transaction
    db.put(&mut wtxn, "seven", &7)?;
    db.put(&mut wtxn, "zero", &0)?;
    db.put(&mut wtxn, "five", &5)?;
    db.put(&mut wtxn, "three", &3)?;
    wtxn.commit()?;
```

The alternative is writing the byte conversion at every call site, and the mistake of writing a value with the wrong byte order is exactly the kind of bug that surfaces as corrupt data rather than as a compile error. Moving the byte order into the type of the database means the conversion is decided once, in one place.

The repository topics name the domain precisely: key-value-store, lmdb, memory-mapping, typed and wrapper. The homepage is docs.rs/heed, so the API documentation is the entry point rather than a separate site.

## Transactions are the unit of work

The example is a small lesson in LMDB's transaction model, and heed follows it closely.

A write transaction is opened on the environment with env.write_txn(), and the database is created inside it with env.create_database(&mut wtxn, None), where None means the default unnamed database. The writes happen inside that transaction, and wtxn.commit() makes them durable. Until that call, the transaction can be dropped and the writes are discarded.

Reads then happen in a read transaction:

```rust
    // We open a read transaction to check if those values are now available
    let rtxn = env.read_txn()?;

    let ret = db.get(&rtxn, "zero")?;
    assert_eq!(ret, Some(0));
```

The get returns an Option, so a missing key is None rather than an error, which is what the assert demonstrates.

Two properties of this model follow directly from LMDB itself and are worth understanding before you commit to it. First, a write transaction blocks other writers, so concurrency is limited by the write path rather than by the read path. Second, and this is the part that surprises people, reads do not block: LMDB is memory-mapped, so a reader sees a consistent snapshot of the database at the moment the read transaction began, and writers proceed in parallel. That snapshot isolation is what makes it attractive for read-heavy workloads, and it is also why a long-lived read transaction will not see later writes.

The README describes the example as storing and reading entries in a safe and ACID way, and points to the examples directory and to a Cookbook for more advanced usage techniques.

## heed and heed3, one codebase and two LMDB branches

The repository publishes two crates that share a codebase. Within the heed3 folder you find the Cargo.toml specific to the heed3 crate, and the conversion between the two is handled by a script.

The reason there are two is upstream. heed supports not only the LMDB mdb.master branch but also the mdb.master3 branch, which features encryption-at-rest. LMDB itself is a C library, and heed binds to it through two sys crates, lmdb-master-sys and lmdb-master3-sys, both of which are workspace members.

The workspace manifest lists the members:

```toml
[workspace]
members = ["lmdb-master-sys", "lmdb-master3-sys", "heed", "heed-traits", "heed-types"]
resolver = "2"
```

Beyond the two sys crates there are heed-traits and heed-types, which is how the codec design separates the trait that defines what a key or value type can do from the concrete type implementations. That separation is what makes Serde-compatible types work: the library is not restricted to the built-in codecs.

For a team choosing between them, the decision is narrow. heed is the default choice for a normal embedded key-value store. heed3 exists for the case where you need the encrypted branch of LMDB. The cost of that choice is that you now depend on a crate that has to be kept in step with the other, and the README's tooling for keeping them in step is a shell script rather than a workspace feature.

## The conversion script is how the fork is maintained

The README describes the mechanism for working on heed3 as a single script, convert-to-heed3.sh.

The script conveniently moves the heed3/Cargo.toml file to the heed folder, updates the heed:: references to heed3::, and generates a commit for easy rollback if needed.

Read carefully, this is not a build tool. It is a text transformation that swaps one crate for the other and rewrites the internal paths, and the fact that it produces a commit with the change in it is the giveaway: the two variants are the same source, prepared for the other target. The generated commit is what makes a rollback possible, because a script that rewrote hundreds of references across a codebase would otherwise leave you reading a diff to undo it.

This has an obvious upside and an obvious cost. The upside is that the encrypted variant cannot drift from the plain one, because it is literally the same code with a different binding target. The cost is that the two crates are close enough that a bug fixed in one is a bug fixed in both, but also that a change to heed requires remembering to run the script, and that the repository carries a shell script performing a mass rename as part of its normal workflow.

If you are evaluating the project, that script is worth reading. It tells you how much of the divergence between the two crates is real and how much is mechanical.

## Building from source needs the LMDB submodules

Building heed locally starts with a recursive clone:

```bash
git clone --recursive https://github.com/meilisearch/heed.git
cd heed
cargo build
```

The recursive flag matters, because the repository carries a .gitmodules file and the LMDB C sources are submodules. If you have already cloned and forgot to initialise them, the README gives the follow-up command:

```bash
git submodule update --init
```

This is the standard shape for a Rust crate that binds to a C library: the C code is vendored as a submodule rather than pulled from a package registry, so the exact upstream commit that was tested is the one you compile. The cost is that a plain git clone gives you a workspace that does not build, which is a common source of confusion for anyone arriving from a pure Rust crate.

The workspace also uses a rustfmt configuration in .rustfmt.toml, and the repository has a GitHub Actions workflow for the Rust build, plus a Discord channel linked from the README badges. Documentation is published to docs.rs, and the dependency status badge points at deps.rs, so the crate's dependency tree is tracked automatically.

One caveat worth stating: the README does not document a rollback or upgrade procedure for an application that already has data written by a previous version of the crate. LMDB's on-disk format is stable in practice, but the crate's own compatibility guarantees are not spelled out in the text available here, so check the release notes before changing versions against a populated database.

## Conclusion

heed suits a Rust service that needs a fast embedded key-value store with a typed API, particularly a team already inside the Meilisearch ecosystem where the same abstraction appears in the search engine itself. It does not suit an application that needs to query by anything other than an exact key, since LMDB is a key-value store and heed exposes no secondary index, and it does not suit a team that needs the encryption-at-rest branch without accepting two closely related crates to maintain between them. Before adopting, add heed to a scratch project and run the documented example against a scratch directory, then decide between heed and heed3 based on whether you need encryption at rest, and check the submodule init step so the LMDB sources are actually present.

## FAQ

### What is heed and what does it wrap?

heed is a set of Rust-centric LMDB abstractions with minimal overhead that enable the storage of Rust types within LMDB, extending support to Serde-compatible types. It also ships heed3, which targets the LMDB master3 branch that features encryption-at-rest.

### How do I open an LMDB database with heed?

You create an environment with EnvOpenOptions::new().open("my-first-db"), open a write transaction, create a database with a typed pair of codecs such as Database<Str, U32<byteorder::NativeEndian>>, put and get values inside a transaction, and commit the write transaction to make the changes durable.

### What is the difference between heed and heed3?

They manage a shared codebase. heed3 is the variant that targets the LMDB mdb.master3 branch with encryption-at-rest, and the convert-to-heed3.sh script moves the heed3 Cargo.toml into the heed folder and rewrites heed:: references to heed3::, generating a commit for easy rollback.

### How do I build heed from source?

Clone the repository recursively with git clone --recursive https://github.com/meilisearch/heed.git, then run cargo build. If you already cloned without the submodules, run git submodule update --init, because the LMDB C sources are vendored as submodules.

### Is heed suitable as a general purpose database?

It suits key-value workloads with exact key lookups. heed exposes the LMDB model, which is a memory-mapped key-value store with snapshot reads and a single-writer transaction model, and it does not add querying or secondary indexes on top of that.

## Sources

- [License: MIT](https://github.com/meilisearch/heed/blob/main/LICENSE)
- [meilisearch/heed on GitHub](https://github.com/meilisearch/heed)
- [Project website](https://docs.rs/heed)
- [README](https://github.com/meilisearch/heed/blob/main/README.md)
- [Releases](https://github.com/meilisearch/heed/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/meilisearch-heed
