MDDB Storage Tuning
How mddb uses the disk, and what to set on each platform so that starts, reads and writes stay fast as a database grows. Numbers marked measured come from mddb's own tests; everything else is a recommendation with its reason.
What mddb asks of the disk
mddb stores everything in one bbolt file:
- Reads go through mmap. A page the process touches for the first time is a page fault and a 4 KiB random read. The page cache is the working set: once a page is cached, reading it costs nothing.
- Every write transaction ends in an fsync. A disk's flush latency, not its bandwidth, decides how many commits a second it can take (DEPLOYMENT).
- The file never shrinks. Deleted pages are reused, not returned to the filesystem.
- An open after a crash walks every page to rebuild the freelist, one random read at a time. On a 101 GB file on a virtual disk that took 101 minutes, at about 17 MB/s.
Since 2.18.2 a clean shutdown stores the freelist, so planned restarts open in
milliseconds. After a crash the server starts at once and serves reads while
the freelist is rebuilt in the background; writes wait for it (503 with
Retry-After). The rebuild reads the file sequentially into the cache first
if it fits in half the available memory, and /health reports progress and
an ETA.
| start, 2.5 GB file, WSL2 ext4, cache dropped (measured) | open |
|---|---|
| walk (2.18.1 behaviour) | 56–60 s |
| after a clean shutdown | 0.014 s |
| after a crash, sequential prewarm first | 1.5 s + 0.4 s |
With MDDB_STORAGE_LAYOUT=per-collection every collection has its own file,
so a deleted collection gives its space back at once and a large collection's
walk does not hold up writes to a small one (STORAGE-LAYOUT).
Rules that hold everywhere
- Let the server shut down. SIGTERM, then wait: the shutdown has a 15 s
budget (
MDDB_SHUTDOWN_TIMEOUT_SEC) and stores the freelist at the end. Docker kills after 10 s by default, so setstop_grace_period: 20s(the bundled compose files do); Kubernetes waits 30 s by default. - Keep
MDDB_FREELIST_SYNCon for large files. Since 2.18.2 it is on by default for a file of 4 GiB or more (MDDB_FREELIST_SYNC_THRESHOLD); set it totruefor smaller ones where crashes happen. It protects the start after a crash, which the clean-shutdown store cannot. It costs a little per commit (0.15 → 0.66 ms on tmpfs, measured), less on a disk that already waits for fsync. - Give it memory for the hot part of the file. A container memory limit counts the page cache; a limit below the working set turns every read into a disk read.
- Use local storage. NFS, SMB, 9p and Docker bind mounts through a VM (VirtioFS, gRPC FUSE) add latency to every page fault and do not give mmap and file locks the guarantees bbolt relies on.
- Do not disable write barriers or fsync unless the database can be rebuilt from its sources.
Disks
| Disk | Random 4 KiB read | What it means for mddb |
|---|---|---|
| Local NVMe | ~0.1 ms | A cold walk runs at hundreds of MB/s. The best choice. |
| SATA SSD | ~0.1–0.2 ms | Fine. |
| Cloud block (EBS gp3, Persistent Disk) | ~0.5–1 ms, capped by provisioned IOPS | gp3's baseline is 3,000 IOPS, about 12 MB/s at 4 KiB. Provision more IOPS for large databases, or use instance-local NVMe. |
| Virtual disk on a laptop (WSL2, Docker Desktop) | ~0.2 ms | About 17 MB/s for a walk, as above. |
| 7200 rpm disk | ~8 ms | A walk runs at 0.5 MB/s. Use freelist sync, and an SSD for anything that restarts. |
Linux
- Filesystem: ext4 or XFS, mounted with
noatime. - I/O scheduler:
nonefor NVMe;mq-deadlinefor SATA SSD and HDD. - Btrfs: copy-on-write fragments a file rewritten in place page by page.
Create the data directory with
chattr +Cbefore the database exists, so the file is not copy-on-write. - ZFS: see ZFS.
- Swap: keep
vm.swappinesslow (10) on a dedicated server, so the kernel drops cold file pages rather than swapping the process out. - Containers: the page cache counts against the cgroup's
memory.max. mddb reads that limit when it decides whether to prewarm.
WSL2
-
Keep the database inside the Linux filesystem (
/home/..., the ext4 VHDX), never under/mnt/c./mnt/cgoes through 9p, which is far slower for random reads and cannot give mmap coherent locking. -
.wslconfig(%UserProfile%\.wslconfig):[wsl2] memory=24GB # the page cache lives inside the VM's memory [experimental] autoMemoryReclaim=disabled # "gradual"/"dropCache" empty the page cache when idle sparseVhd=true # give space back to Windows after deletesWith automatic reclaim on, an idle server loses its cache and the next queries read from disk again.
-
Windows Defender: exclude the distribution's
ext4.vhdxfrom real-time scanning; every write to the database is a write to that file. -
Docker Desktop: use a named volume, not a bind mount from Windows.
macOS
- Commits are slower than on Linux, by design. Go's
fsyncon macOS isF_FULLFSYNC, which waits until the drive has written its cache to the media; plainfsyncon macOS does not. Expect milliseconds per commit, and load in batches (add-batch,ingest) rather than one document per request. - Spotlight: exclude the data directory (System Settings → Spotlight →
Privacy, or
touch <dir>/.metadata_never_index). Indexing a file that changes on every commit costs CPU and I/O for nothing. - Time Machine: exclude the database (
tmutil addexclusion <file>). Every commit dirties pages, so each hourly backup copies the whole file again, and a copy taken mid-write is not a consistent backup. Back up withGET /v1/backup(BACKUP). - Docker Desktop: use a named volume; VirtioFS bind mounts add latency to every page fault.
FreeBSD
- UFS: soft updates with journaling (the default),
noatime. - ZFS: see below. ZFS is the common choice and needs the most attention.
ZFS
On Linux and FreeBSD alike:
recordsize=16Kon the dataset holding the database (set it before creating the file). With the default 128K, every 4 KiB page fault reads and checksums a 128K record.atime=off,compression=lz4(cheap, and bbolt pages compress well),logbias=latency.- ARC and the page cache both hold mmapped pages. Cap the ARC
(
zfs_arc_maxon Linux,vfs.zfs.arc_maxon FreeBSD) so that it and the page cache together fit in memory. sync=standardwith a SLOG for a database that is the only copy of its data;sync=disabledonly for one that can be rebuilt (DEPLOYMENT).
Windows
- Keep the database on a local NTFS volume on SSD.
- Exclude the data directory from Defender real-time scanning.
- The same rules about shutdown apply: stop the service, do not kill it.
GPU
mddb does not use a GPU. Vector search runs on the CPU with SIMD (AVX2 on
amd64, NEON and SME on arm64), and an HNSW query takes well under a
millisecond, so a GPU would add a transfer for no gain. Embeddings are where
a GPU helps: point mddb at an embedding server that runs on one, such as
Ollama with CUDA or Metal (MDDB_EMBEDDING_PROVIDER=ollama,
config).