bbolt is an embedded, pure-Go B+tree key-value store, originally written by Ben Johnson as BoltDB and now maintained by the etcd team as bbolt. It is to Go what LMDB is to C: a single-writer, mmap-based, B+tree key-value store with strict ACID semantics and remarkable simplicity. bbolt is the storage engine inside etcd, Consul, InfluxDB’s metadata, Open Policy Agent, and many other Go infrastructure tools.
The rest of this page explains the on-disk layout, how copy-on-write commits keep the file crash-safe, a complete Go example, and the pitfalls that matter in production.
A bbolt database is a single file divided into fixed-size pages, normally the operating system page size. The first two pages are meta pages, each holding a transaction ID, the root page of the bucket tree, the freelist location and a checksum. Data lives in B+tree pages: branch pages hold keys and child pointers, and leaf pages hold keys and values sorted byte-wise. Each bucket is its own B+tree, and buckets can nest. Readers access the file through a read-only memory map, so a read transaction is essentially a consistent pointer to one meta page and costs no locks or copies.
A write transaction never modifies pages in place. It copies every page it touches, plus the path up to the root, into memory, then writes those pages to free locations in the file. After syncing data, it writes the new meta page into whichever meta slot holds the older transaction ID and syncs again. On open, bbolt picks the meta page with the highest valid transaction ID, so a crash at any point leaves either the old or the new state, never a mix. Pages freed by a transaction cannot be reused while any read transaction that might still see them is open.
package main
import (
"bytes"
"fmt"
"log"
"time"
bolt "go.etcd.io/bbolt"
)
func main() {
db, err := bolt.Open("app.db", 0600, &bolt.Options{Timeout: time.Second})
if err != nil {
log.Fatal(err)
}
defer db.Close()
// read-write transaction: create bucket and store keys
err = db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("users"))
if err != nil {
return err
}
if err := b.Put([]byte("u:1001"), []byte(`{"name":"alice"}`)); err != nil {
return err
}
return b.Put([]byte("u:1002"), []byte(`{"name":"bob"}`))
})
if err != nil {
log.Fatal(err)
}
// read-only transaction: prefix scan with a cursor
db.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte("users")).Cursor()
prefix := []byte("u:")
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
fmt.Printf("%s = %s\n", k, v)
}
return nil
})
}
Update commits if the function returns nil and rolls back otherwise; View is read-only. Keys are sorted as bytes, so a cursor Seek followed by Next gives efficient range and prefix scans. The Timeout option makes Open fail instead of blocking forever if another process holds the file lock.
| Store | Engine | Pick when |
|---|---|---|
| bbolt | B+tree, mmap, single writer | Go program, read-heavy metadata, strict transactions |
| BadgerDB | LSM tree, concurrent writers | Write-heavy Go workloads |
| LMDB | B+tree, mmap, single writer | Same design for C and other languages |
| RocksDB | LSM tree, highly tunable | Large datasets with heavy writes |
| Redis | In-memory server | Shared cache or data across processes |
bbolt compact to rewrite it.View functions short.See the Key-Value Stores overview, LevelDB for the LSM design that RocksDB extends, and FoundationDB for a distributed transactional key-value store.