Open-source project
kriszyp/lmdb-js avatar
kriszyp/lmdb-js

lmdb-js: a memory-mapped key-value store with synchronous reads for JavaScript

Simple, efficient, ultra-fast, scalable data store wrapper for LMDB

684 stars54 forksJavaScriptNOASSERTION

At a glance

What is it?
lmdb-js wraps the LMDB memory-mapped database behind a JavaScript API published to npm as the `lmdb` package, translating JS values into binary keys and values in native code. It suits Node, Deno and Bun workloads that want an ACID key-value store with promise-based writes, and it is the wrong choice if you cannot ship a native addon.
Who is it for?
Adopt lmdb-js when you have an in-process key-value workload in Node, Deno or Bun that needs durability without a database server, and you can accept a native addon in your install path. Do not adopt it when you need a pure JavaScript dependency tree, when your deployment target cannot run prebuilt binaries, or when you want something other than a single-writer embedded store with a very small API surface.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Published to npm as lmdb, and built for three runtimes

The library is called lmdb-js, but that is not the name you install. The library is published to the NPM package `lmdb`, and that the 1.x versions were published to `lmdb-store`. So a dependency written today carries a name that says nothing about its origin.

The runtime reach is the point. The same package works in Node, Bun and Deno, and the design is built on the Node-API, which the documentation describes as the route to maximum compatibility across all supported Node versions and current Deno and Bun versions. On top of that sits a layer of accelerated functions for direct V8 interaction, compiled for and automatically loaded in Node v16. Every other runtime uses the standard Node-API functions, which the documentation still describes as providing excellent performance.

There is a fallback for people on older Node versions who want the faster path, and it is a build flag rather than a different package. The documentation tells you to use `npm install --build-from-source` for absolute maximum performance on older versions of Node.

Deno users load it through the npm module identifier, which downloads the package:

js
import { open } from 'npm:lmdb';

The package also supports both native ESM and CommonJS usage, and the `package.json` shows the shape of that: the root export has separate `require` and `import` conditions, a `node` condition pointing at `dist/index.cjs` and `node-index.js`, and a `default` of `index.js`, with TypeScript declarations exposed for both module systems.

Synchronous reads and asynchronous writes, and the reason for the split

The design section explains an unusual choice in a JavaScript library, and the explanation is the interesting part.

Reads are synchronous. The argument is that LMDB is a memory-mapped database, so reading or writing inside a transaction does not use I/O other than the slight possibility of a page fault, and can usually be performed faster than the event queue callbacks can even execute. On that basis, it is easier to write code that gets instant synchronous values from reads.

Writes are asynchronous, and the reason is that committing a transaction does involve I/O. The library therefore routes transactions through an asynchronous batching process and returns a promise that resolves once the data has been written and flushed to disk.

Batching is not just a convenience. The documentation argues that when a database is under load, slower transactions allow more writes per transaction, and the library can drive LMDB towards its maximum throughput while keeping fully synced operations, which preserves the durability and safety of the transactions.

There is a feature list behind this design: high-performance translation of JS values to and from binary key and value data, promise-based API for queued off-thread writes, simple transaction management, iterable queries and cursors, record versioning with optimistic locking, and optional native off-main-thread compression using LZ4. The documentation also claims that integrated serialisation and deserialisation can make retrieval several times faster than `JSON` alone, and its badges advertise 8.5 MOPS for get and 1.7 MOPS for put.

Conditional writes as the path to multi-process safety

The concurrency story runs through conditional writes. The library supports and encourages them, and the stated purpose is to allow atomic operations that depend on previously read data.

Most transactional operations can be expressed with an optimistic-locking pattern: read a record, decide something about it, then write it back with a condition that the record has not changed in the meantime. If the condition fails, the write is rejected rather than silently overwriting a concurrent change.

This matters because the writes are executed off the main thread. The documentation makes the connection explicit: the conditional write pattern is what allows the library to delegate writes to off-thread execution and scale to handle concurrent execution across many processes or threads while maintaining data integrity. Without a condition, a background write thread would be a data race waiting to happen.

Two smaller design points sit in the same section. The library automatically handles database growth, expanding the file size with a heuristic that aims to minimise fragmentation. And the default syncing configuration keeps LMDB's crash-proof behaviour, where a machine can be turned off at any point without corrupting the data, while the batching still preserves that guarantee.

The practical upshot is that the API surface is small but assumes a particular style: read synchronously, decide, then write conditionally and await the promise.

LZ4 compression on the same thread that does the write

Compression is optional and enabled with a single option on the database instance. The implementation detail is the interesting part: LZ4 compression works in conjunction with the asynchronous writes by performing the compression in the same off-main-thread thread that performs the writes in a transaction.

That placement avoids a second thread hop and avoids compressing on the main thread, so the cost stays off the event loop either way. The documentation justifies the choice on speed, noting that LZ4 is extremely fast and that decompression can be performed at roughly 5GB/s, so storage efficiency comes at a negligible performance impact.

The trade-off is that compression is not free in terms of CPU time, it is simply off the critical path. For a workload storing large blobs where the transaction thread is already busy, turning it on could show up as slower write throughput rather than faster.

The repository layout gives a sense of how the pieces are separated. There are `open.js`, `read.js`, `write.js`, `keys.js`, `level.js` and `caching.js` at the top level, with `native.js` and `node-index.js` handling the native and Node-specific entry points, and `src/` holding the C++ sources that `binding.gyp` compiles. `util/`, `dict/`, `dependencies/` and `benchmark/` hold supporting code, and `test/` holds the test suite.

Installing the package and opening your first database

Installation is a single npm command, because the package name on the registry is the short one:

bash
npm install lmdb

The usage example in the documentation is short enough to read in full, and it shows the shape of the API: `open` comes from the main module, options are passed in the constructor, and a value is stored with `put` and read back with `get`.

js
import { open } from 'lmdb'; // or require
let myDB = open('my-db', {
	// any options go here, we can turn on compression like this:
	compression: true,
});
await myDB.put('greeting', { someText: 'Hello, World!' });
myDB.get('greeting').someText; // 'Hello, World!'
// or
myDB.transaction(() => {
	myDB.put('greeting', { someText: 'Hello, World!' });
	myDB.get('greeting').someText; // 'Hello, World!'
});

Two details in that snippet are worth pausing on. The `put` is awaited, because the write is queued and the promise resolves after the data is flushed to disk. The `get` is not awaited, because the read is synchronous and returns the value directly. And the transaction variant shows the same calls inside a `transaction` callback, where a group of operations commits together.

The `compression: true` option in the constructor is the switch described above, and it is commented as an example of what belongs in the options object.

Prebuilt binaries, node-gyp and the packaging pipeline

Because this is a native addon, the install path is more than a JavaScript download. The `package.json` sets the install script to `node-gyp-build-optional-packages`, which is the mechanism that prefers a prebuilt binary and only falls back to compiling when it has to. The package also ships a `download-lmdb-prebuilds` binary in its `bin` section, which points at `bin/download-prebuilds.js`.

The build scripts show the rest of the pipeline. The `build` script runs `node-gyp` for configure and build and then `rollup -c` to bundle the JavaScript, and `rebuild` does the same plus copies the type declarations with a rename to `index.d.cts`. Publishing runs `rollup`, the declaration copy, `prebuildify-ci download`, a script that sets optional dependencies, and the test suite.

Prebuilds are produced for several targets. The scripts include `prebuild-libc-musl` with `ENABLE_V8_FUNCTIONS=false` and a musl tag, and `prebuild-libc` with a target of 20.18.0, both using `--napi` and `--platform-packages`. The musl build with V8 functions disabled exists precisely so that Alpine-style images get a binary that will run, which is a detail worth knowing if you deploy on a musl base image.

TypeScript support is part of the package rather than an add-on, with `index.d.ts` and `index.d.cts` in the files list, and a `tsd` configuration pointing at `test/types` for type tests.

Where lmdb-js is the wrong choice

The largest constraint is the native addon. There is no pure JavaScript build path, so a deployment pipeline that forbids native code, a platform without a matching prebuild, or a runtime you cannot compile on will force you to build from source. The `install` script is a native build step in disguise, and on some locked-down hosts that is an operational burden rather than a detail.

The second constraint is the shape of the API. This is a key-value store with a deliberately small surface. There is no query language, no schema layer and no network protocol, so anything that needs secondary indexes or relational-style querying has to be built on top, in your code. The library is described as being used as a high-performance cache for builds in Parcel and for Kibana, as the storage layer for HarperDB and for Gatsby's database, and as a search and analytical engine for clinical medical research, which is a good indication of the workloads it fits: embedded, local and hot.

The third is the licence ambiguity, which is a documentation problem rather than a technical one. The licence badge and the `package.json` both say MIT, while the repository's licence field is marked as a custom licence that GitHub cannot classify, with a pointer to the LICENSE file. If your organisation needs a recognised licence identifier, read that file before you vendor it.

On maintenance the picture is healthy. The repository is not archived, the last push was on 2026-10-20, and the release cadence is quick: v3.5.8 on 2026-10-20, v3.5.7 on 2026-10-02 and v3.5.6 on 2026-06-18. A `SECURITY.md` file is present at the root. What you do not get from the documentation is a stated support policy for older Node versions, so treat the prebuild matrix as the practical answer to that question.

Editorial conclusion

Adopt lmdb-js when you have an in-process key-value workload in Node, Deno or Bun that needs durability without a database server, and you can accept a native addon in your install path. Do not adopt it when you need a pure JavaScript dependency tree, when your deployment target cannot run prebuilt binaries, or when you want something other than a single-writer embedded store with a very small API surface. First check that a prebuilt binary covers your platform, since the package falls back to compiling the native code, verify the `compression: true` path against your own data before enabling it in production, and read the LICENSE file yourself, because GitHub's licence classifier could not categorise it even though package.json declares MIT.

Frequently asked questions

What is the npm package name for lmdb-js?

The library is published to the npm package `lmdb`, installed with `npm install lmdb`. The 1.x versions were published to `lmdb-store`.

Does lmdb-js work in Deno and Bun?

Yes. The library is based on the Node-API for compatibility across supported Node versions and current Deno and Bun versions, and in Deno it is loaded through the npm module identifier with `import { open } from 'npm:lmdb';`.

How do I enable compression in lmdb-js?

Pass `compression: true` in the options object to `open`. It uses LZ4, and the compression is performed in the same off-main-thread thread that performs the transaction writes.

Why are reads synchronous and writes asynchronous in lmdb-js?

Reading inside a transaction in a memory-mapped database does not use I/O beyond a possible page fault, so it is often faster than the event queue callbacks can run, while committing a transaction does involve I/O. Writes are therefore queued and batched off the main thread, and the returned promise resolves once the data is written and flushed to disk.

Does installing lmdb-js require a compiler?

Usually not. The install script is `node-gyp-build-optional-packages`, which prefers a prebuilt binary and falls back to compiling when needed. The README also points to `npm install --build-from-source` for the fastest path on older Node versions, and the package ships a `download-lmdb-prebuilds` command.

Official sources

  1. Issues
  2. kriszyp/lmdb-js on GitHub
  3. README
  4. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kriszyp-lmdb-js.svg)](https://hysenlabs.com/projects/kriszyp-lmdb-js)

Was this useful?