Embedded key-value stores in 2026: revisit RocksDB, LMDB and Pebble before you build another storage layer

Sooner or later, every backend team meets a problem that looks too small for a database server and too big for a JSON file. A device has to keep readings while it is offline. A service needs a local index of a few hundred million keys. A queue consumer must remember what it has already processed, even after a crash. Somebody says, “Let us just use RocksDB”, somebody else says, “LMDB is simpler”, and a third person has read that a Go team wrote its own engine. Two weeks later there is a storage layer in the codebase that nobody fully understands.

This post is written for the moment before that storage layer is built. An embedded key-value store is a library, not a server. It runs inside your process, writes files into a directory you choose, and gives you sorted keys, values and some form of atomic write. That sounds simple, but the engine you pick decides how much your disk writes for every byte you store, how much space you need for the same data, what happens when the power goes off, how you take a backup, and how hard the next upgrade will be.

We revisit three engines in 2026: RocksDB (the log-structured merge engine from Meta, the base of many databases), LMDB (the memory-mapped B+tree from the OpenLDAP project) and Pebble (the RocksDB-inspired engine written in Go for CockroachDB). We also look briefly at bbolt, SlateDB, redb and fjall, as context. The aim is not to crown a winner. It is to understand how each engine works, which trade-off it makes, and what living with it for five years looks like.

A few pieces of news set the scene. LMDB released version 1.0.0 on 30 June 2026, the first new major line in many years, followed by 1.0.1 on 6 August and 1.0.2 on 8 September. The on-disk format changed, and 0.9 and 1.0 files cannot be opened by each other. The 0.9 line is still maintained (0.9.36 came out on 6 August 2026). RocksDB is moving fast in its 11.x line: 11.0.0 in February 2026 dropped support for very old table formats, and the latest GitHub release is v11.12, published on 26 October 2026. Pebble’s v2 release branch had v2.1.7 on 24 August 2026, described in its release notes as the version used in production by CockroachDB 25.3.x.

This post stays with embedded engines that you link into your own program. If what you need is a shared cache or a network key-value server, start with Redis, Valkey and Dragonfly. If you want SQL in a single file, SQLite in production in 2026 is the better starting point. If your data has outgrown one machine, read distributed SQL in 2026 and wide-column databases in 2026. Many of the engines inside those systems are exactly the ones discussed here.

A scenario to keep us honest

We will use one invented company through the post. Ponmudi Fleet Systems is an invented example, not a real business. It is a Thiruvananthapuram company, working out of a technology park, that builds telematics for about 2,600 trucks, buses and ambulances across Kerala and Tamil Nadu. The company has about 210 people. The platform team has six engineers, led by Lakshmi.

What they run today:

  • A small box in each vehicle: an ARM board with 1 GB of RAM and 16 GB of eMMC flash. A Go agent on the box reads GPS and vehicle data every two seconds and keeps it in a local bbolt file until it can upload.
  • On the ghat roads and in some forest stretches there is no mobile coverage for hours. After floods or long trips, a box may hold two or three days of readings before it uploads.
  • A Go ingestion service in the data centre. It removes duplicate readings, keeps the latest state of each vehicle and a 90-day index of trip events. Today all of this sits in one large PostgreSQL table with heavy inserts and deletes.

What triggered the review: in the last monsoon, about 40 boxes ran out of storage. The cause was a firmware bug that kept a read transaction open for days, so freed pages could not be reused and the bbolt file kept growing. Around the same time, the PostgreSQL table behind the ingestion service crossed 600 GB, and nightly deletes of old rows started to fight with daytime inserts. A senior engineer proposed “one storage engine everywhere: RocksDB on the boxes and in the service”, and another proposed Pebble because the whole stack is Go.

Lakshmi’s questions: which engine should run on a small flash device, which inside a write-heavy Go service, what will each one cost in disk writes and disk space, how do we back them up and upgrade them, and can six of us own this for five years?

The short version

  • Pick the engine by workload shape, not by fame. Log-structured merge (LSM) engines like RocksDB and Pebble turn random writes into sequential ones and compress well, but they rewrite data in the background (compaction). B+tree engines like LMDB and bbolt read fast and need no background work, but each write touches whole pages and the file does not shrink on its own.
  • RocksDB is the most complete and most configurable embedded engine. Column families, pessimistic and optimistic transactions, a backup engine, several compaction styles and blob storage for large values are all in the box, under a dual GPLv2 or Apache 2.0 licence. The price is tuning and a C++ dependency. It releases often and the 11.x line removed some old formats and APIs.
  • LMDB is small, fast for reads, and has no background threads, no log and no recovery step: a crash leaves the last committed state. It allows one writer at a time, needs a maximum map size up front, and grows if long read transactions block page reuse. LMDB 1.0 adds checksums, encryption, incremental backup and larger pages, but needs a dump and load to move from 0.9.
  • Pebble is a focused LSM engine in pure Go, built for and run in production by CockroachDB. It avoids cgo and has strong range delete support. It deliberately leaves out RocksDB features it does not need, including transactions, column families and a backup engine, and its format moves only forward.
  • Benchmarks lie by omission. Every vendor number you will read is from the vendor’s own run, on its own data size, cache size and sync settings. Measure write amplification and space on your own data before you choose.
  • For Ponmudi Fleet Systems, our plan is: fix the open read transaction and cap the backlog on the boxes, staying on a B+tree store there; first try PostgreSQL time partitioning for the ingestion table; and only if that is not enough, build the dedup index on Pebble, with time-ordered keys and range deletes, on a pinned release branch. RocksDB stays the choice if they ever need transactions, column families or non-Go callers. LMDB fits a read-mostly reference file shared by many processes.

Where things stand on 1 November 2026

RocksDB

RocksDB is developed by Meta’s database engineering team and builds on Google’s LevelDB. Its README describes it as a library for a fast key-value server, especially suited to flash storage, with an LSM design and “flexible tradeoffs” between write, read and space amplification. It is dual licensed: you may use it under GPLv2 or under Apache 2.0, at your choice.

The latest GitHub release is v11.12, published on 26 October 2026. The change log on the main branch already lists 11.13.0, dated 21 October 2026, but there is no GitHub release for it yet, so treat it as upcoming. Earlier this year:

  • 11.0.0 (23 February 2026) stopped reading and writing table files with a format version below 2. The upgrade path for very old databases is a full compaction using a version between 4.6.0 and 10.x first. It also removed deprecated APIs and added a table option that stores keys and values separately in a data block; files written with it are rejected by older versions.
  • 10.11.0 (23 January 2026) made table format version 7 the default. That format has been readable since 10.4.0, which matters if you ever need to roll back.
  • 11.8.0 (28 July 2026) added callback-based asynchronous reads, with an io_uring event loop in the Posix file system.
  • 11.13.0 (upcoming) changes one behaviour worth noting: if automatic recovery is disabled, a retryable I/O error during flush or compaction now stops writes until you call Resume, where earlier it was retried quietly.

The cadence is roughly one minor version a month. Language bindings often lag. On our box, the Python binding rocksdict 0.3.29 reported RocksDB 9.8.4 in its log file.

LMDB

LMDB (Lightning Memory-Mapped Database) is developed by Howard Chu and Symas for the OpenLDAP project, under the OpenLDAP Public License 2.8. Development happens in the OpenLDAP git repository; the GitHub repository is a mirror.

The big news is LMDB 1.0. The 1.0.0 release came on 30 June 2026, 1.0.1 on 6 August with fixes (including Windows write fixes and a page split size calculation fix) and 1.0.2 on 8 September. The upgrade notes are short and blunt: the on-disk format has changed, 0.9 and 1.0 are mutually incompatible, and the way across is to export with the 0.9 mdb_dump and import with the 1.0 mdb_load. New in 1.0:

  • Incremental backup, made possible because every page header now carries the ID of the transaction that wrote it.
  • Optional page-level checksums and encryption at rest, including keyed checksums and authenticated encryption, through a module interface that the command line tools also understand.
  • Use of raw block devices directly, without a file system.
  • Two-phase commit, so LMDB can take part in a distributed transaction.
  • Page sizes up to 64 KB, and key size limits that follow the page size (0.9 had a fixed 512 byte maximum key).
  • Read-only transactions nested under a write transaction.

The 0.9 line is still maintained: 0.9.36 came out on 6 August 2026. Bindings are adapting carefully. The Python binding py-lmdb 3.0.0 (2 October 2026) bundles both 0.9.36 and 1.0.2, opens an existing file with whichever engine matches its format, and still creates new files in the 0.9 format unless you ask for 1.0.

Pebble

Pebble is Cockroach Labs’ key-value engine, written in Go and licensed under the BSD 3-clause licence. Its README is honest about its purpose: it is “focused on performance and internal usage by CockroachDB”. It was an optional engine in CockroachDB 20.1 (May 2020) and became the default in 20.2 (November 2020).

Releases are tagged from release branches. The v2 module path is github.com/cockroachdb/pebble/v2. v2.1.7 came out on 24 August 2026 and its release note says it is the release used in production by CockroachDB 25.3.x. Earlier, v2.1.6 and v2.0.9 were released together on 27 May 2026. The master branch moves ahead with newer CockroachDB versions. Pebble v2 cannot open databases created by RocksDB; old stores must first be moved forward through format upgrades using older Pebble versions.

Around them

  • bbolt is the etcd project’s maintained fork of Ben Johnson’s Bolt, which was originally a port of LMDB to Go. It is a pure Go B+tree with one writer and many readers, under the MIT licence. v1.5.0 came out on 21 June 2026. Our invented company uses it on its boxes today.
  • SlateDB is an LSM engine in Rust that writes to object storage. It is young (v0.17.0 on 29 September 2026) and worth watching if your data should live in S3 compatible storage.
  • redb (v4.4.0 on 13 October 2026) is a B+tree engine in Rust, and fjall (3.1.13 on 25 October 2026) is an LSM engine in Rust. Both are under the Apache 2.0 licence and are good options for Rust teams; we do not cover them in depth here.

The first questions to ask

Before comparing engines, write down answers to these seven questions. They decide more than any benchmark.

  1. What is the read to write ratio, and are writes random or in key order? Time-ordered keys behave very differently from random IDs.
  2. How big is the data now, and in three years, compared with RAM? An engine that is excellent when data fits in memory may behave differently at ten times RAM.
  3. How do you delete? One key at a time, whole ranges, or by expiry? Deletes are where LSM and B+tree engines differ most.
  4. What does “committed” mean for you? Must a write survive a power cut, or only a process crash? This decides your sync setting and your write latency.
  5. How many writers and processes? One process with many threads, or several processes sharing one file?
  6. What language calls the engine? A C++ library in a Go or Python service brings build, profiling and memory tooling questions.
  7. How will you back it up, check it and upgrade it? If the answer is “copy the directory”, read the backup section below first.

How each one works

LSM trees in one minute

An LSM engine never updates data in place. A write goes first to a write-ahead log (WAL) on disk and to an in-memory sorted table called the memtable. When the memtable is full, it is written out as a sorted, immutable file (an SST, sorted string table). Over time there are many such files, arranged in levels. A background process called compaction merges files, keeps the newest version of each key, drops deleted and overwritten entries, and writes new files. Reads check the memtable first, then files from newest to oldest, using Bloom filters to skip files that cannot contain the key.

The benefit is that the disk sees large sequential writes and data compresses well in big blocks. The cost is that the same byte is written several times as it moves down the levels, and deleted data keeps using space until compaction reaches it. A delete is itself a write: a small marker called a tombstone.

RocksDB: the LSM engine with every knob

RocksDB started from LevelDB 1.5 and added what large deployments needed: multi-threaded compaction, column families (separate key spaces with their own settings sharing one WAL), merge operators for read-modify-write without a read, range deletes, prefix Bloom filters, a block cache, compression per level, BlobDB for storing large values outside the main tree, and several compaction styles:

  • Leveled compaction (the default) keeps each level about ten times larger than the one above. It gives low space amplification and good reads, at the cost of higher write amplification.
  • Universal compaction merges whole sorted runs. It lowers write amplification but raises space amplification, and the RocksDB wiki warns that a compaction may temporarily need as much extra space as the data itself. The wiki also asks for care above about 100 GB per column family.
  • FIFO compaction simply drops the oldest files, useful for caches and short-lived time series.

RocksDB also ships transactions (described below), checkpoints and a backup engine. It is used inside many systems you may already run. That breadth is its strength and its weight: the OPTIONS file that a default database writes on our box listed about 200 settings.

LMDB: a B+tree in a memory map

LMDB keeps all data in one file organised as a B+tree, and maps the whole file into the process address space. Reads walk the tree through the memory map: no copying into a separate cache, and the operating system’s page cache does all the caching. Values returned by a read can point straight into the map, which is why the documentation says “look but do not touch”.

Writes use copy-on-write. A write transaction never changes a live page; it writes new copies of the pages it touches, up to a new root, and then commits by updating one of two meta pages. Until that meta page is written, readers keep seeing the old tree. If the process or machine dies before that point, the old meta page still points to a complete, consistent tree. This is why LMDB has no log and no recovery step.

Pages that are no longer needed go on a free list and are reused by later writes, but only once no reader can still see them. The file itself does not shrink. Its size is a high-water mark, and the map size you set is the upper limit of the database. The default map size is only 10 MB, so every real application sets it.

Pebble: a focused LSM in Go

Pebble uses the same LSM ideas and inherits RocksDB’s file formats, with extensions. Cockroach Labs’ 2020 introduction explains why it exists: CockroachDB is written in Go, crossing into C++ through cgo had a cost, debugging a large foreign C++ codebase during severe incidents was hard, and they wanted control over features such as range deletions.

Pebble implements the subset that CockroachDB needs: batches (including indexed batches you can read from before commit), snapshots, iterators with bounds, checkpoints, leveled compaction, range deletions, merge operator, single delete, SST ingestion and Bloom filters. It adds its own improvements, which the README lists: sublevels in L0 and split flushes to reduce read cost during heavy writes, delete-only compactions that drop whole files covered by a range deletion, block property filters that let iterators skip irrelevant blocks, and range keys.

What it leaves out is just as important. The README lists features it does not have: backups, column families, transactions, universal and FIFO compaction, persistent cache and sub-compactions, among others. It also carries a clear warning that Pebble may silently corrupt data if used on a RocksDB database that uses a feature Pebble does not support.

Amplification: the three numbers that decide everything

The RocksDB tuning guide uses three ratios, and they apply to every engine here.

Write amplification

Bytes written to storage divided by bytes written by the application. If your app writes 10 MB/s and the disk shows 30 MB/s, write amplification is 3. In an LSM tree, data is rewritten as it moves down levels. The tuning guide’s own worked example for a leveled setup with a multiplier of 10 arrives at a total of about 33. In a B+tree, changing a 100 byte value rewrites at least one whole page (4 KB by default on Linux) plus the pages on the path to the root, so small random writes can be expensive too. On flash with limited write endurance, such as the eMMC in a vehicle box, this number is about hardware lifetime, not just speed.

Read amplification

The number of reads needed to answer one query. A B+tree lookup touches one page per level of the tree, and the upper levels are almost always in memory. An LSM lookup may check the memtable, several L0 files and one file per lower level; Bloom filters skip most of those for point reads but cannot help range scans.

Space amplification

Size on disk divided by size of live data. An LSM tree holds old versions and tombstones until compaction removes them; with leveled compaction the tuning guide’s example comes to about 1.14. A B+tree holds partly filled pages and free pages, and its file stays at its largest size unless you make a compacting copy.

What this means in practice

  • Heavy random writes on large data: LSM usually wins on write amplification and space after compression.
  • Read-heavy work that fits in RAM: a memory-mapped B+tree is very hard to beat.
  • Queue-like data (write, read once, delete): both struggle in different ways. In an LSM, choose keys so that whole ranges can be dropped. In a B+tree, cap the backlog so the file’s high-water mark stays acceptable.

Transactions, concurrency and crash safety

RocksDB

Every WriteBatch is atomic. For more, RocksDB offers a pessimistic TransactionDB, which locks keys and suits heavy contention, and an OptimisticTransactionDB, which checks for conflicts at commit and suits low contention. Snapshots give consistent reads.

Durability is a setting. The RocksDB documentation says plainly that by default each write is asynchronous: it returns once the data is handed to the operating system. A process crash does not lose it, but a power cut or kernel crash can lose the most recent writes. Set sync to true on writes that must survive power loss, or batch writes and sync per batch. Many teams discover this default only after an incident.

LMDB

LMDB transactions are fully ACID with MVCC. There is one write transaction at a time across all threads and processes using the environment; other writers wait. Readers never block writers and writers never block readers, and several processes on the same host can open the same file.

By default every commit syncs. Flags such as NOSYNC and NOMETASYNC trade durability for speed, and the header file warns that with NOSYNC a system crash can lose the last transactions or even corrupt the database. Two practical rules from the documentation: do not open the same environment twice in one process (the POSIX file locks get confused), and keep read transactions short, because an open read transaction keeps its view of the data alive and stops those pages from being reused. That second rule is exactly what bit our invented company’s bbolt files, since bbolt has the same design. LMDB provides mdb_reader_check to clear entries left behind by dead reader processes.

Pebble

Pebble has atomic batches, indexed batches and snapshots, but no transaction API. Cockroach Labs’ post explains that CockroachDB never used RocksDB’s transactions; it builds distributed transactions on top of atomic batches. If your application needs conflict detection, you build it yourself or pick RocksDB. Durability is explicit on each commit: you pass a Sync or NoSync write option.

Performance and how to read benchmarks

What vendors publish

  • RocksDB publishes benchmark results and tools (db_bench) on its wiki. These are the project’s own runs on its own hardware and settings.
  • Pebble publishes nightly benchmarks, and its 2020 introduction showed YCSB results where Pebble met or exceeded RocksDB. That was Cockroach Labs’ own run, with options set to be similar, and with data that fit in memory.
  • LMDB material from Symas emphasises read speed and small code size (about 32 KB of object code). Again, the project’s own claims.

None of these are wrong, but none are your workload. We do not repeat any of their numbers as a verdict here.

Questions to ask of any benchmark

  1. Did the data fit in RAM? Most published comparisons that show huge read numbers do.
  2. Were writes synced? An unsynced LSM write and a synced B+tree commit measure different things.
  3. How long did it run? LSM engines look best in the first minutes, before compaction debt builds up. Run for hours and watch for write stalls.
  4. What were key and value sizes, and were keys random or sequential?
  5. Was compression on, and was the data compressible? Random bytes do not compress; real JSON and logs do.
  6. Which binding was used? A Python or Go wrapper adds its own overhead and may bundle an older engine version.

What to measure yourself

  • Bytes written to disk per byte written by the app (from /proc/diskstats or iostat).
  • Disk usage after a day of your real overwrite and delete pattern, before and after compaction.
  • p99 write latency with your real sync setting, and whether it spikes during compaction.
  • Memory use, including the page cache. For LMDB, the resident memory of the process includes mapped pages, which confuses many dashboards.
  • Time to open after a crash, with a large WAL for LSM engines.

Licences

RocksDB: GPLv2 or Apache 2.0

You choose one of the two. Most companies pick Apache 2.0, which allows embedding in closed source products with notice and patent terms. Bindings have their own licences: check the wrapper you use.

LMDB: OpenLDAP Public License 2.8

A short permissive licence in the BSD family: keep copyright notices and include the licence text in distributions. Symas sells support. There is no paid edition with extra features; everything is in the open source code.

Pebble: BSD 3-clause

Permissive. Note that this is separate from CockroachDB’s own licensing; Pebble the library is BSD licensed. The practical risk with Pebble is not the licence but the roadmap, which follows CockroachDB’s needs.

Around them

bbolt is MIT. SlateDB, redb and fjall are Apache 2.0.

Operations and day two

Tuning

  • RocksDB: start from the defaults of a recent version and change little. The settings that matter most are the block cache size, write buffer size and count, compaction style, level size targets (max_bytes_for_level_base and the multiplier, or dynamic level sizing), compression per level, and the number of background jobs. Read the write stalls page: RocksDB slows or stops writes when there are too many memtables, too many L0 files or too many pending compaction bytes.
  • LMDB: set the map size generously (it is address space, not memory), choose sync flags deliberately, set the maximum number of readers and named databases, and in 1.0 consider the page size. That is nearly all.
  • Pebble: the Options struct covers cache size, memtable size, L0 thresholds, level targets and concurrent compactions. The defaults follow CockroachDB’s experience. Pebble’s Metrics give compaction, flush and per-level numbers you can export.

Backups

  • RocksDB: checkpoints create a consistent snapshot directory, using hard links for SST files on the same file system, so they are cheap. BackupEngine manages incremental backups with checksums and rate limiting, and can write to another file system. Copying the live directory with cp or rsync is not a backup.
  • LMDB: mdb_copy (or the API equivalent) makes a consistent copy of a live environment; the compacting option drops free pages. LMDB 1.0 adds incremental backup through mdb_dump and mdb_load. Because a copy holds a read transaction, a long copy stops page reuse while it runs.
  • Pebble: no backup engine. Use Checkpoint to create a consistent snapshot directory, then copy it elsewhere with your own tooling.

Upgrades and file formats

This is where embedded engines surprise teams. Your data files outlive your binary.

  • RocksDB: new versions read old formats for a long time, but not forever (11.0 dropped format versions below 2). New defaults may write files that older versions cannot read; 10.11 made format version 7 the default. Before upgrading, check how far back you could roll if needed, and pin format settings if rollback matters.
  • LMDB: 0.9 to 1.0 is a dump and reload, with downtime or a dual-write plan. Within a line, upgrades are usually drop-in. Watch your bindings’ defaults; py-lmdb 3.0 still creates 0.9 files by default.
  • Pebble: format major versions ratchet forward. The README is clear that the upgrade is permanent, with no way back to an earlier format. Some migrations run in the background through compactions; some block until done. Pebble v2 refuses formats older than it supports, so a very old store needs a stepwise upgrade through older Pebble versions.

Monitoring

  • RocksDB: the LOG file in the database directory, the rocksdb.stats property with per-level compaction statistics, the statistics object, and the ldb and sst_dump tools.
  • LMDB: mdb_stat shows page counts, tree depth, free pages and reader slots. Alert on file size against map size and on stale readers.
  • Pebble: DB.Metrics, the event listener for flushes, compactions and write stalls, and the pebble command line tool.
  • For all: disk bytes written per app byte, free disk space with headroom for compaction or copies, and open time after restart.

Side by side

Data structure

  • RocksDB: LSM tree, leveled by default, with universal and FIFO options.
  • LMDB: copy-on-write B+tree in a memory-mapped file.
  • Pebble: LSM tree, leveled only, with L0 sublevels.

Best fit

  • RocksDB: large, write-heavy data on SSDs, many key spaces, when you need transactions or the widest feature set.
  • LMDB: read-heavy data that mostly fits in RAM, shared by several processes on one host, where simplicity matters.
  • Pebble: Go services with write-heavy data and range deletes, run by a team comfortable following a focused upstream.

Concurrency

  • RocksDB: many concurrent writers in one process; one process opens a database for writing (secondary and read-only instances exist).
  • LMDB: one writer at a time, unlimited readers, multiple processes.
  • Pebble: many concurrent writers in one process.

Transactions

  • RocksDB: pessimistic and optimistic transactions, plus atomic batches.
  • LMDB: full ACID transactions, nested write transactions, two-phase commit in 1.0.
  • Pebble: atomic and indexed batches only.

Crash safety default

  • RocksDB: WAL on, but writes not synced unless you ask.
  • LMDB: commits synced by default; consistent state after any crash.
  • Pebble: you choose Sync or NoSync on each commit.

Space behaviour

  • RocksDB and Pebble: grow with overwrites and deletes until compaction; need free space for compaction.
  • LMDB: file stays at its high-water mark; free pages are reused; compacting copy to shrink.

Operational weight

  • RocksDB: highest. Many options, frequent releases, C++ build.
  • LMDB: lowest. Few settings, no background work.
  • Pebble: medium. Fewer options than RocksDB, pure Go, but upgrades follow CockroachDB’s format path.

A worked plan for Thiruvananthapuram

Here is how we would approach Ponmudi Fleet Systems, month by month.

Month 1: measure and stop the bleeding.

  • Fix the firmware bug that held a read transaction open. Add a watchdog that logs and closes any read transaction older than a few minutes.
  • Cap the on-box backlog. At one reading every two seconds, three days is about 130,000 readings per vehicle. Size the cap from real record sizes, leave space for the file’s high-water mark, and decide what to drop first if the cap is hit (for example, keep one reading in five for the oldest day).
  • On a test box, measure bytes written to eMMC per day with the current bbolt file.

Month 2: keep the box simple.

  • Stay on a B+tree store on the boxes. The write rate is tiny (a few hundred bytes every two seconds), there is no background compaction to compete with uploads on a 1 GB board, and recovery after a sudden power cut is simply the last committed transaction.
  • Batch readings into one commit every few seconds instead of one commit per reading, to cut page rewrites on flash.
  • Do not move the boxes to RocksDB. It would add a C++ cross-compile, a block cache and compaction threads on a small board, and gives little for this write rate.

Month 3: try the boring fix in the data centre first.

  • Partition the PostgreSQL table by day and drop old partitions instead of deleting rows. For many teams this removes the nightly delete fight without a new storage layer.

Month 4: only if partitioning is not enough, prototype on Pebble.

  • Use keys that start with the day and vehicle ID, so that the 90-day expiry becomes one range deletion per day, which Pebble can turn into delete-only compactions that drop whole files.
  • Pin to a Pebble release branch (v2.1.x today), read its release notes before each update, and test format upgrades on a copy, since they cannot be undone.
  • Take Checkpoints every hour to a separate disk and copy them off the machine. Practise restore every month.
  • Export Pebble metrics, alert on write stalls and on free disk space below twice the largest expected compaction.

What we did not choose and why.

  • RocksDB in the ingestion service: a fine engine, but the team is all Go, does not need transactions or column families, and would rather not own a cgo build. It stays the answer if they later need those features or call the store from another language.
  • LMDB in the ingestion service: the dedup index is write-heavy with constant expiry, which is the B+tree’s weakest pattern, and LMDB allows one writer at a time. LMDB is, however, a good fit for one thing Lakshmi mentioned: a nightly route and geofence reference file that several processes read all day. Build it once, open it read-only everywhere.

When not to adopt each

Do not adopt RocksDB if

  • Your data is small and read-mostly; you will pay tuning and build cost for nothing.
  • Nobody on the team will read the LOG file and compaction statistics.
  • You cannot accept a C++ dependency, or your binding lags far behind the engine.
  • Disk is tight and you cannot keep headroom for compaction.

Do not adopt LMDB if

  • You have many concurrent writers that must not wait for each other.
  • The workload is mostly random overwrites and deletes of data much larger than RAM.
  • You cannot set a sensible maximum size up front, or your platform limits address space (32-bit systems).
  • Your code tends to hold read transactions open for a long time.

Do not adopt Pebble if

  • You need transactions, column families or a backup engine and do not want to build them.
  • Your service is not written in Go.
  • You need a feature CockroachDB does not need; it is unlikely to be added.
  • You want to open existing RocksDB data directly; v2 does not support that.

A short history

  • 2009 to 2011: LMDB’s design was discussed on the OpenLDAP developer list as a replacement for Berkeley DB. The first public version appeared in the OpenLDAP source repository in June 2011, and Howard Chu presented the MDB paper at LDAPCon 2011.
  • 2012 to 2013: Facebook started RocksDB by forking LevelDB 1.5 and borrowing ideas from Apache HBase. The GitHub repository dates from November 2012.
  • 2013 onwards: Ben Johnson’s Bolt brought an LMDB-like design to Go; etcd later forked it as bbolt.
  • July 2017: RocksDB changed to its current dual GPLv2 or Apache 2.0 licence.
  • 2018: the Pebble repository was created at Cockroach Labs, starting from an unfinished Go port of LevelDB.
  • 2020: Pebble became optional in CockroachDB 20.1 (May) and the default in 20.2 (November).
  • 2026: RocksDB 11.0 removed very old formats; LMDB 1.0 shipped with a new format, checksums and encryption; Pebble’s v2 line serves CockroachDB 25.3.x.

Decision guide

You need a local buffer on a small device

Use a B+tree store (LMDB, or bbolt for pure Go, or redb for Rust). Batch commits, cap the backlog, keep read transactions short.

You need a large, write-heavy store inside a service

Use an LSM engine. Pick RocksDB for the widest features and language support, Pebble for a Go service that fits its feature set. Design keys so expiry is a range delete.

You need fast reads of reference data shared by several processes

Use LMDB. Build the file in one writer, open it read-only in every reader process, and let the page cache do the work.

You need SQL, joins or ad hoc queries

Do not build on a key-value engine. Use SQLite or PostgreSQL.

You need the data on more than one machine

Do not build replication yourself on top of an embedded engine. Use a system that already does it, many of which run RocksDB or Pebble inside.

Checklist

  • Workload written down: read and write ratio, key order, value sizes, delete pattern, growth.
  • Durability decided: which writes must survive power loss, and the sync setting that matches.
  • Key design reviewed for range scans and range deletes.
  • Write amplification and space measured on real data for at least a day.
  • Map size (LMDB) or compaction headroom (LSM) sized with margin.
  • Read transactions and iterators closed promptly; a watchdog for long ones.
  • Backup method uses checkpoints, BackupEngine or mdb_copy, never a plain copy of a live directory.
  • Restore practised and timed.
  • Engine and binding versions pinned; format settings recorded; rollback limits known.
  • Metrics exported: disk bytes written, file sizes, compaction or write stall events, reader slots.
  • Licence of engine and binding checked.
  • One owner named for the storage layer.

Common mistakes

  • Assuming RocksDB writes are durable by default. They reach the operating system, not the disk, unless sync is on.
  • Copying a live database directory as a backup. Use the engine’s checkpoint or copy tool.
  • Leaving a read transaction open in LMDB or bbolt. The file grows until the disk or map size runs out.
  • Benchmarking for five minutes. LSM compaction debt appears later.
  • Deleting expired data key by key in an LSM. Millions of tombstones slow reads until compaction; use range deletes and time-prefixed keys.
  • Forgetting the binding. A wrapper may bundle a much older engine than the one whose release notes you read.
  • Upgrading formats without a rollback test. Pebble format upgrades and new RocksDB defaults may not be readable by the old binary.
  • Opening the same LMDB environment twice in one process. Share one environment handle.
  • Building a database. An embedded engine plus replication, schema and query code quickly becomes a database that only your team maintains.

A small hands-on check

We ran a small, local drill on our box on 1 November 2026, around 2:20 PM IST. It used Python 3.13 with py-lmdb 3.0.0 (LMDB 1.0.2 and 0.9.36) and rocksdict 0.3.29 (which reported RocksDB 9.8.4), plus a Go program using Pebble v2.1.7, all with default options. No servers and no network were involved. It is a toy, not a benchmark: one run each, on a virtual machine where fsync is cheap, with random values that do not compress.

The workload: write 200,000 keys with 100 byte values in synced batches of 1,000, overwrite every key once, delete every second key, then compact. We summed file sizes in each database directory.

  • RocksDB: about 22.8 MB after loading, 45.6 MB after the overwrite and 47.0 MB after the deletes. After a flush and full manual compaction it fell to about 12.7 MB, almost all in one SST file. Old versions and tombstones held space until compaction.
  • LMDB 1.0.2: about 25.8 MB after loading and 26.1 MB after the overwrite, and still 26.1 MB after deleting half the keys, because the file keeps its high-water mark. A compacting copy was 25.8 MB: it drops free pages but does not repack half-empty pages. LMDB 0.9.36 gave the same sizes.
  • Pebble: about 31.8 MB after loading, 55.4 MB after the overwrite and 59.0 MB after the deletes. After flush and compaction, the SST files came to about 11.4 MB. The directory still held four WAL files of about 11.9 MB in total, which Pebble keeps for recycling, so the directory size was about 32.4 MB.

We also ran a crash test: a child process committed synced batches and reported each one, and we killed it with SIGKILL after about 1.5 seconds. On reopen, all three engines had every acknowledged batch and nothing from the batch in flight. This tests a process crash only. The operating system kept unsynced data in memory, so it says nothing about power loss, which needs real hardware tests.

The timings we saw are not worth printing as a comparison; the point is the shape of space use, which matches the theory above. The drill scripts and raw output are kept on our box for anyone who wants to repeat it.

Unlearn, re-learn

  • Unlearn “RocksDB is the default fast storage engine”. Re-learn that it is the most configurable LSM engine, and that configurability is a cost you pay every year.
  • Unlearn “memory-mapped means unsafe”. Re-learn that LMDB’s copy-on-write design gives a consistent file after any crash with default settings; the risks come from turning off sync and from long read transactions.
  • Unlearn “a delete frees space”. Re-learn that in an LSM a delete is a write that frees space later, and in a B+tree it frees pages for reuse but not the file size.
  • Unlearn “pure Go means toy”. Re-learn that Pebble runs at scale in CockroachDB, and that its narrow feature set is a design choice you must accept.
  • Unlearn “upgrading a library is a dependency bump”. Re-learn that the files on disk are part of the contract; LMDB 1.0, RocksDB 11 and Pebble’s format versions all prove it.
  • Practise: take one store you run today, measure its bytes written per app byte and its size after a week of deletes, and write down how you would restore it. Then decide whether it is in the right engine.

Sources

RocksDB

LMDB

Pebble

Around them

comments powered by Disqus

Releted Posts

Graph databases in 2026: revisit Neo4j Community, Memgraph and Apache AGE before you model another relationship in SQL joins

Most teams do not decide to adopt a graph database. They arrive at it through a query. Somebody asks “which customers are connected to that blocked customer through a shared phone number, bank account or address?

Read more

MySQL-compatible databases in 2026: revisit MySQL, MariaDB and Percona Server before you upgrade another database

Most teams that run MySQL do not choose their database every year. They chose it once, long ago, and since then they have only upgraded it, or not upgraded it.

Read more

In-process analytics in 2026: revisit DuckDB, Polars and chDB before you spin up another warehouse for a small question

Think of a knitwear exporter in Tiruppur that makes T-shirts, polos and kids’ wear for buyers in Europe, the US, the Gulf and Japan (an invented example, not a real client).

Read more