lmdb
lmdb, commonly called lmdb-js, is an embedded key-value database for Node.js, Bun, and Deno backed by the native LMDB engine. It memory-maps local files, gives JavaScript synchronous reads and batched asynchronous durable writes, and stores objects through MessagePack by default. Ordered primitive and compound keys support range scans, while transactions, record versions, named sub-databases, compression, backups, and multi-process access cover workloads beyond a simple cache.
lmdb-js is a strong embedded store when ordered keys, synchronous reads, and local multi-process access match the workload. Do not choose it merely because benchmarks are fast; native deployment, key design, page-fault latency, and transaction lifetime become application responsibilities.
Use it if
- You need a very fast local persistent key-value store with synchronous reads inside a Node process
- Your workload uses ordered keys and range scans rather than relational joins, ad hoc predicates, or a network query protocol
- Several local processes or worker threads must share one ACID database file without running a separate database server
- You store JavaScript objects and want integrated MessagePack encoding, optional LZ4 compression, and optimistic version checks
- You need SQL, secondary indexes, joins, migrations, or ad hoc reporting: LMDB exposes ordered key-value ranges, so every query shape and index is your data-modeling responsibility
- You need a database shared across machines or directly reachable by browsers: this is an embedded local-file engine, not an authenticated client-server database
- Your deployment cannot run native addons or download a matching prebuilt optional package: 3.5.6 publishes platform binaries, but unsupported targets fall back to a C++ build toolchain
- You cannot tolerate synchronous page faults on the event loop: reads are intentionally synchronous, and the README recommends prefetch or getMany when cold pages may hit slow storage
- Your code holds long transactions or awaits arbitrary work inside write callbacks: LMDB has one writer, async callbacks delay commit, and long read transactions prevent free-space reclamation
Setup reality
npm install lmdb pulls the JavaScript package plus a platform-specific optional native package for supported macOS, Linux, and Windows architectures. CI images that omit optional dependencies, unusual libc or CPU targets, and future runtimes may fall back to a source build requiring a compiler, Python, make, and Node addon headers. The database path is operational state. A path containing a dot is treated as a file, while other paths are treated as directories; mount it on persistent local storage, give the process write permission, and do not put it on a network filesystem without proving LMDB locking and memory mapping are supported there. Reads are synchronous and usually memory-speed, but a cold page can block the event loop. Use prefetch or getMany before latency-sensitive batches whose working set may not be resident. Writes return Promises and are automatically batched during an event turn; await the Promise when later code depends on a durable commit. Do not put unrelated awaits inside transaction callbacks because the single write transaction stays open and other operations can enter a surprising order. Explicit read snapshots must always call done, or reader slots and reclaimable pages remain pinned. Pick encodings once. Default MessagePack preserves more JavaScript types than JSON; string and binary databases return different shapes, compression must be enabled consistently every time the files are opened, and cbor requires cbor-x. Default ordered keys have a 1,978-byte maximum, strings cannot contain a null character, and number keys use JavaScript doubles. Named databases require maxDbs capacity and should not be mixed with ordinary entries in the root keyspace. Each process has its own optional object cache, so multi-process cache users need validated reads or external invalidation. Call close during orderly shutdown and use backup for a safe snapshot instead of copying live files blindly.
Patterns
Open a database and store an objectopen-put-get
import { open } from 'lmdb';
const db = open('./data/app.lmdb', {
encoding: 'msgpack',
});
await db.put('user:42', { name: 'Ada', active: true });
const user = db.get('user:42');get is synchronous. Await put before relying on a committed value when caching is not enabled.
Remove a key durablydelete-key
const removed = await db.remove('user:42');
console.log('removed:', removed);remove is queued and returns a Promise like put; awaiting it waits for the write transaction to commit.
Let same-turn writes share a transactionbatch-writes
const writes = [
db.put('job:1', { status: 'queued' }),
db.put('job:2', { status: 'queued' }),
db.put('job:3', { status: 'queued' }),
];
await Promise.all(writes);Asynchronous puts issued in the same event turn are automatically batched; serially awaiting each put prevents that batching opportunity.
Update related values in one transactionatomic-update
const purchased = await db.transaction(() => {
const item = db.get('inventory:shoe');
if (!item || item.count === 0) return false;
db.put('inventory:shoe', { ...item, count: item.count - 1 });
db.put(['orders', orderId], { sku: 'shoe' });
return true;
});Keep the callback synchronous and short. Awaiting network or timer work holds the single write transaction open.
Scan an ordered compound-key rangerange-scan
for (const { key, value } of db.getRange({
start: ['orders', customerId, 0],
end: ['orders', customerId, Number.MAX_SAFE_INTEGER],
})) {
console.log(key[2], value);
}Compound array keys sort element by element. Design key prefixes around every range query the application must answer.
Protect an update with record versionsoptimistic-write
const versioned = open('./data/versioned.lmdb', { useVersions: true });
const entry = versioned.getEntry('settings');
const saved = await versioned.put(
'settings',
{ ...entry.value, theme: 'dark' },
entry.version + 1,
entry.version,
);
if (!saved) throw new Error('settings changed concurrently');The fourth put argument is the required previous version. A mismatch resolves false instead of overwriting concurrent work.
Separate keyspaces with named databasesnamed-databases
const root = open('./data/app', { maxDbs: 10 });
const users = root.openDB('users');
const sessions = root.openDB('sessions', { encoding: 'json' });
await users.put(42, { name: 'Ada' });
await sessions.put('token-1', { userId: 42 });Named databases consume maxDbs slots and share one environment. Avoid normal application records in the root keyspace once it holds named-database metadata.
Hold a consistent read snapshotconsistent-read-snapshot
const transaction = db.useReadTransaction();
try {
const before = db.get('account:1', { transaction });
await prepareReport();
const sameSnapshot = db.get('account:1', { transaction });
} finally {
transaction.done();
}Always call done. Long-lived snapshots consume reader capacity and prevent old pages from being reclaimed.
Prefetch keys before synchronous readsprefetch-cold-keys
const keys = ['user:1', 'user:2', 'user:3'];
await db.prefetch(keys);
const users = keys.map((key) => db.get(key));Prefetch moves likely hard page faults off the main thread; it is most useful when the working set may not be cached by the operating system.
Compress larger values off the main threadenable-compression
const compressed = open('./data/documents.lmdb', {
compression: { threshold: 1024 },
encoding: 'msgpack',
});
await compressed.put('doc:1', largeDocument);Every opener of these files must use compatible compression settings or values cannot be decoded correctly.
Create a safe database backupsnapshot-backup
await db.backup('./backups/app-2026-08-08.lmdb');Use the backup API for a consistent snapshot rather than copying live memory-mapped files with an ordinary file-copy command.
Close after outstanding writes finishclose-database
async function shutdown() {
await db.flushed;
await db.close();
}
process.once('SIGTERM', () => {
shutdown().then(() => process.exit(0));
});close is asynchronous and waits for outstanding transactions. Coordinate shutdown so no request starts a new operation during closing.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| better-sqlite3 | npm | You need an embedded local database but SQL, indexes, joins, and migrations fit the data better |
| classic-level | npm | You prefer the standard Level ecosystem API and a simpler ordered key-value abstraction |
| keyv | npm | You need a small cache API with TTL support and interchangeable storage adapters rather than direct LMDB control |