# hashicorp/go-memdb: an in-memory database for Go built on immutable radix trees

> go-memdb gives Go programs ACID transactions, MVCC reads and secondary indexes without a server. It is a library, not a database you run, and the README is explicit that it does not provide durability.

**hashicorp/go-memdb** — Golang in-memory database built on immutable radix trees

- Repository: https://github.com/hashicorp/go-memdb
- Website: https://pkg.go.dev/github.com/hashicorp/go-memdb
- Stars: 3,475 · Forks: 235
- Language: Go
- License: MPL-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/hashicorp-go-memdb

## What go-memdb solves, and who it is for

Most Go programs that need indexed lookups over structured data reach for SQLite or an external database, which brings a file, a driver and a query language. go-memdb offers the middle position: a table-and-index store that lives entirely in the process, with no server, no file format and no SQL parser. The package README describes it as "a simple in-memory database built on immutable radix trees" that provides Atomicity, Consistency and Isolation from ACID.

The audience is Go developers writing long-running services that already have a durable source of truth and need a fast, queryable projection of it in memory, plus tests and tools that need a real indexed store without a fixture database. The README states the trade-off plainly: "Being that it is in-memory, it does not provide durability." That single sentence rules it out for anything where losing the process means losing the data. It also means the schema is declared in Go rather than in DDL, so the shape of your data and the shape of your indexes are the same code.

The module is github.com/hashicorp/go-memdb, licensed MPL-2.0, and go.mod declares go 1.23 with a single direct dependency, github.com/hashicorp/go-immutable-radix v1.3.1, plus github.com/hashicorp/golang-lru v0.5.4 as an indirect one. The dependency footprint is small enough to read.

## How the immutable radix tree gives you MVCC without reader locks

The mechanism is the part worth understanding before adopting it. Each index is backed by a radix tree from go-immutable-radix. An update does not mutate nodes in place; it produces a new tree that shares the untouched subtrees with the old one. A write transaction therefore accumulates a set of new roots, and commit publishes them. Readers that started before the commit keep traversing the old roots and see a consistent snapshot.

That is where the README's claim comes from: "By leveraging immutable radix trees the database is able to support any number of concurrent readers without locking, and allows a writer to make progress." Readers do not take a lock, and one writer does not block them. What the model does not give you is multiple concurrent writers. Writes are serialized, which is the usual shape for this kind of store and is worth knowing before you design around parallel ingestion.

The schema is a map of table names to TableSchema values, each holding a map of indexes. An IndexSchema carries a Name, a Unique flag and an Indexer. The README example uses StringFieldIndex on an Email field for a unique id index and IntFieldIndex on Age for a non-unique one. Indexes are not optional decoration: they are the only way queries reach data, so a table with no index that matches a query shape leaves you iterating everything. The README also notes that "Certain types like UUID can be efficiently compressed from strings into byte indexes for reduced storage requirements", which matters when the in-memory footprint is the constraint.

Watches are the other mechanism. Callers "populate a watch set as part of a query", and the set can later report whether a modification affected that query's results. That is a change-detection primitive rather than a subscription bus, and the repository layout reflects it: watch.go, watch_few.go and a watch-gen/ directory sit alongside the core files, with watch_few.go suggesting a variant tuned for small watch sets.

## Installing go-memdb and running a first indexed query

There is no binary and no service to start. You add the module to a Go project and import the package. The go.mod in the repository pins go 1.23, so a toolchain at or above that is the safe assumption.

```bash
go get github.com/hashicorp/go-memdb
```

After that command the module appears in your go.mod and the memdb package is importable. The README's example is the shortest path to a working store: declare a struct, describe the tables and indexes, open the database, write in a transaction, then read in a read-only transaction.

```go
schema := &memdb.DBSchema{
	Tables: map[string]*memdb.TableSchema{
		"person": &memdb.TableSchema{
			Name: "person",
			Indexes: map[string]*memdb.IndexSchema{
				"id": &memdb.IndexSchema{
					Name:    "id",
					Unique:  true,
					Indexer: &memdb.StringFieldIndex{Field: "Email"},
				},
			},
		},
	},
}
db, err := memdb.NewMemDB(schema)
```

The write side is explicit about intent: db.Txn(true) opens a write transaction, txn.Insert("person", p) adds objects, and txn.Commit() publishes them. Until commit, the README states, "the updates are not visible". The read side uses db.Txn(false) and should be closed with defer txn.Abort().

```go
txn := db.Txn(true)
for _, p := range people {
	if err := txn.Insert("person", p); err != nil {
		panic(err)
	}
}
txn.Commit()

txn = db.Txn(false)
defer txn.Abort()
raw, err := txn.First("person", "id", "joe@aol.com")
```

With the schema and data from the README, First returns the Person whose Email is joe@aol.com and the printed greeting is "Hello Joe!". Iteration uses txn.Get("person", "id"), which walks the index in key order; the README's output lists Dorothy, Joe, Lucy and Tariq, which is alphabetical by email rather than insertion order. Range queries use txn.LowerBound("person", "age", 25) and then stop when the value exceeds the upper bound, which is how the README produces the 25 to 35 window. Note that this requires the age index to exist; without it there is nothing to bound against.

## Where go-memdb is the wrong tool

The durability gap is the first limit and it is not a configuration you can turn on. The README states the database does not provide durability, so a process exit, a panic in an unrelated goroutine or a machine reboot loses everything. Any design that treats go-memdb as the system of record will lose data. It works as a cache, a projection or a test double, not as the place the truth lives.

The second limit is scope. Everything is inside one process, so two services cannot share a dataset, and there is no wire protocol, no client library and no replication. If you need a second reader outside the process, go-memdb is the wrong layer.

The third is query capability. There is no SQL, no join and no ad hoc query planning. The schema you declare is the set of access paths you get. A query shape with no matching index degrades to scanning the table, and the README does not describe a planner that will rescue you. Compound indexes exist, but you have to design them up front for the queries you know about.

Memory is the fourth. Every object and every index entry is resident, and the README's note about compressing UUIDs into byte indexes exists precisely because that footprint is a real constraint. There is no eviction, no spill to disk and no size ceiling other than the machine. Finally, write concurrency is serialized, so a workload that needs many writers making progress at once is not what this store is shaped for.

## go-memdb compared with Buntdb, ramsql and SQLite

The searches around this project tend to pair it with Buntdb, ramsql and SQLite, and the differences are in the data model rather than in speed. Buntdb is a key-value store with an in-memory index and optional persistence to an append-only file, so it answers the durability question that go-memdb explicitly leaves open, at the cost of a key-value model instead of tables with typed secondary indexes. If your access pattern is keys and ranges over keys, Buntdb is closer; if it is several indexes over the same records, go-memdb's schema is the more direct fit.

ramsql takes the opposite route: it presents a SQL interface in memory. That means you keep SQL, a driver and the ability to change queries without recompiling the schema, but you take on a SQL layer and whatever it supports. go-memdb asks you to express queries as index lookups in Go, which is less flexible at the query layer and more predictable at the type layer, since objects come back as your own structs rather than rows you scan.

SQLite, whether through a Go driver or a pure-Go implementation, is the durable option in this comparison. It gives you SQL, transactions, indexes and a file that survives restarts. The reason to pick go-memdb over it is not capability but placement: no file, no driver, no cgo question, and snapshots for readers that cost nothing to take. If any of the durability, SQL or multi-process requirements apply to you, SQLite is the better answer and go-memdb is not a substitute for it.

## Maintenance, releases and what the licence means for your build

The repository is not archived, and the last push was on 2026-06-28. The release history is uneven: v1.3.3 in May 2022, v1.3.4 in October 2022, then v1.3.5 on 2025-02-27. That is a long gap between tags, so the version number alone tells you little about the current state of the code; the commit history on main is the better signal, and the CHANGELOG.md in the repository root is where the project records what changed between them.

Upgrade cost is low in one sense and non-obvious in another. The dependency surface is one direct module, go-immutable-radix, so there is little transitive risk to audit. But the API is a schema you write by hand, and index definitions are part of your source. A change to an indexer or to a table's index set is a code change and a redeploy, not a migration script. Because there is no persistence, there is also no migration path to write, which removes an entire category of upgrade work.

The licence is MPL-2.0, a file-level copyleft licence. Modifications to files covered by it carry obligations when those files are distributed, while larger works that combine it with other code are treated differently. This is a description of the licence family, not legal advice; if you vendor or modify the package, read the LICENSE file in the repository and decide with whoever handles licensing on your side. Consuming it as an unmodified module dependency is the ordinary case.

## Conclusion

Adopt go-memdb when the working set fits in memory and you want indexed, transactional access to it inside a single Go process, for example as the state store behind a service that already persists elsewhere. Do not adopt it when you need data to survive a restart, when several processes must share one dataset, or when you want SQL. Before committing, verify the index schema covers every query shape you plan to run, because a table without a matching index forces a full scan, and check the CHANGELOG for what changed after v1.3.4.

## FAQ

### What is go-memdb?

It is the memdb package from HashiCorp, an in-memory database for Go built on immutable radix trees. It provides atomicity, consistency and isolation from ACID, but the README states it does not provide durability.

### How do I install go-memdb in a Go project?

Add the module with go get github.com/hashicorp/go-memdb and import the memdb package. There is no server or binary to install, since it is a library that runs inside your process.

### Does go-memdb persist data to disk?

No. The README says that being in-memory, the database does not provide durability, so data is lost when the process exits. It is suitable for caches, projections and tests rather than as a system of record.

### How do I query data in go-memdb?

You open a read transaction with db.Txn(false) and then use First, Get or LowerBound against a named index, such as txn.First("person", "id", "joe@aol.com"). Iteration walks the index in key order, and range scans start at LowerBound and stop when the value passes your upper bound.

### Can several goroutines read go-memdb at the same time?

Yes. The README states that immutable radix trees let the database support any number of concurrent readers without locking while a writer makes progress. Writes themselves are applied through transactions that become visible on commit.

## Sources

- [hashicorp/go-memdb on GitHub](https://github.com/hashicorp/go-memdb)
- [License: MPL-2.0](https://github.com/hashicorp/go-memdb/blob/main/LICENSE)
- [Project website](https://pkg.go.dev/github.com/hashicorp/go-memdb)
- [README](https://github.com/hashicorp/go-memdb/blob/main/README.md)
- [Releases](https://github.com/hashicorp/go-memdb/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hashicorp-go-memdb
