Durable writes: every write must survive a crash
When a write must survive a power cut, the engine flushes the disk on every commit. badger and sqlite win by batching many commits into one flush.
This is the workload where the data must not be lost: a ledger, a queue you cannot drop, anything where a power cut in the next second must not erase the write you just acknowledged.
To guarantee that, the engine forces the disk to physically flush before it tells you the write succeeded. That flush is the most expensive thing a storage engine does, and it changes the ranking completely. The engines that win the raw write-ingest race are not the ones that win here.
Every engine on this page is run in the same mode: a real disk flush on every single commit. That is the FULL durability regime, same rules for everyone, so the numbers are directly comparable. This is the honest cost of durability, and it is why these figures are in the hundreds to tens of thousands rather than the millions.
The numbers
Durable writes, flush on every commit, 1 KB values, 8 concurrent clients, on the Apple M4:
| Engine | Shape | Durable writes/sec | p99 | How |
|---|---|---|---|---|
| badger | LSM | 27,365 | 1.6 ms | Groups concurrent commits into one flush |
| sqlite | B-tree | 18,025 | 3.6 ms | Groups concurrent commits into one flush |
| goleveldb | LSM | 1,144 | 28 ms | One flush per commit |
| pebble | LSM | 1,048 | 27 ms | One flush per commit |
| tamnd/kv | hash-log | 525 | 50 ms | One flush per commit |
| pogreb | hash-log | 402 | 67 ms | One flush per commit |
| buntdb | in-memory B-tree | 261 | 103 ms | One flush per commit |
| bbolt | B+tree | 109 | 264 ms | One flush per commit |
The gap at the top is not a faster disk, it is a smarter commit. badger and sqlite practice group commit: when eight clients commit at once, the engine collects them and flushes the disk once for the whole batch, so eight durable writes cost one flush. The other engines flush per commit, so they hit the disk's physical flush ceiling, a few hundred to about a thousand per second on this hardware, no matter how many clients are waiting.
This is the one place where tamnd/kv's per-commit fsync shows: at 525 durable writes/sec it sits mid-pack, well behind the group-committing engines. Its strict mode does coalesce concurrent writers onto a shared fsync, so a burst pays one flush between them, but it does not batch as aggressively as badger and sqlite, and this workload measures exactly that gap. If durable write throughput under concurrency is your bottleneck, that batching is the feature to look for.
What to pick
- badger for durable writes with the lowest tail (1.6 ms p99) and an LSM's friendly write path.
- sqlite if you also want SQL and transactions; it is the second-fastest here through the same group-commit trick.
- Either one any time many clients commit concurrently and every commit must be safe.
What to avoid
- bbolt for high-rate durable writes. At 109 per second it is the floor here, because a crash-safe B-tree copies a path of pages and then flushes on every commit.
- Reading a background-flush write number (from the ingest page) as if it were per-commit durable. An engine doing millions of writes/sec on a background flush does a hundred to a few tens of thousands with a flush on every commit. Always check which regime a write number is.
A note on the two regimes
Out of the box, these engines disagree about durability: some flush on every commit, some flush on a short timer, none of them lose data outright. That is why kvbench measures two regimes and never mixes them in one table.
DEFAULT runs each engine at its own shipped durability, the honest out-of-the-box comparison.
The timer-flush engines, badger, pebble, goleveldb and tamnd/kv, acknowledge a write before the disk has it and flush a moment later, so a crash loses at most a bounded sub-second sliver, the same contract Redis gives with appendfsync everysec.
This page is the other regime, FULL, where every engine is forced to flush on every commit so the disk is in the loop for all of them equally.
tamnd/kv is durable in both: its default trades a sub-second worst-case loss window for the throughput on the ingest page, and its SyncWrites mode gives the zero-loss per-commit guarantee measured here.
The methodology explains how the two regimes are kept comparable.