Feather DB File Format: From v3 to v9 — What Changed
Six format versions, one embedded file. Feather DB's .feather format evolved from a plain float32+HNSW layout (v3) to a fully self-contained persisted-graph format (v9) that cold-loads a 500K-node index in 48ms. Here's every change that mattered.
One file that does everything
Feather DB has always been a single-file database. The entire persistent state — vectors, HNSW graph, typed edges, metadata, quantization factors, namespace tables — lives in one .feather binary. Copy it to move a database. Delete it to drop one.
That constraint has driven every file format decision since v3. The format has to be self-describing, backward-compatible, and fast to open. From v3 through v9, it evolved through six distinct versions to hit all three. This post covers every version, what it added, and the benchmark numbers that motivated each change.
The version history at a glance
| Version | Released | Key change | HNSW on load |
|---|---|---|---|
| v3 | v0.10.0 | Initial stable format. Float32 vectors, HNSW graph, flat metadata. | Rebuild from scratch |
| v4 | v0.11.0 | Typed context graph edges. Breaking change from v3. | Rebuild from scratch |
| v5 | v0.12.0 | Recall count tracking added to metadata. Non-breaking. | Rebuild from scratch |
| v6 | v0.13.0 | Section index introduced. Prior versions were fully sequential. Breaking change. | Rebuild from scratch |
| v7 | v0.14.0 | Namespace table. Optimized variable-length HNSW neighbor list encoding. | Rebuild from scratch |
| v8 | v0.15.0 | Per-vector int8 scale factors. Metadata offset table for O(log n) lookup. Parallel load via FEATHER_LOAD_THREADS. | Parallel rebuild (4.79× faster) |
| v9 | v0.16.0 | Persisted HNSW graph embedded in file. Per-modality persist_graph flag. int8 modalities get int8 graph with exact round-trip. | Direct load — no rebuild |
v3 to v7: the base era
The v3 format established the structure that all later versions built on: a fixed-width magic + version header, followed by sections for the vector store, HNSW graph, metadata, and context graph edges.
The critical design choice in this era — one that had to be revisited in v9 — was how the HNSW graph was stored. The file persisted the raw graph data (neighbor lists, level assignments, the global entry point), but the in-memory graph structure was always reconstructed from those bytes on DB.open(). Reconstruction means allocating the in-memory nodes, wiring up the neighbor pointers, and validating HNSW invariants. For a large index, this is expensive CPU work.
The v6 section index was the most structurally important change in this era. Before v6, sections were written sequentially with no directory: a reader had to scan from the start to find any section. The section index — a table of (section_type, byte_offset, byte_length) triples written immediately after the header — lets DB.open() seek directly to any section. It also made the format extensible: unknown section types are skipped, so future versions can add sections without breaking older readers.
v7 added the namespace table and tightened the HNSW neighbor list encoding to variable-length arrays, reducing file size for sparse upper-level nodes.
v8: int8 quantization and parallel load
v8 shipped with Feather DB v0.15.0 and addressed two problems simultaneously: memory footprint and cold start latency.
int8 quantization support
v8 added a per-vector int8 quantization section to the file. Each float32 vector can be stored alongside its int8 encoding and a single float32 scale factor. When set_int8_ram() is called after DB.open(), Feather quantizes the in-memory graph in-place, keeping vectors as int8 with per-vector max_abs scale.
At 60K × 768-dim, RAM drops from 227 MB (float32) to 129 MB (int8) — a 1.76× reduction. Recall@10 moves from 0.972 to ~0.88. For context retrieval workloads where you're surfacing 5–10 chunks, 0.88 is fine.
Parallel HNSW load
The more operationally impactful v8 change was parallel graph reconstruction. The graph build phase — wiring up in-memory neighbor pointers from the serialized edge lists — parallelizes nearly linearly: each node's neighbor list is independent of every other node's reconstruction.
The FEATHER_LOAD_THREADS environment variable controls the thread count:
import os
import feather_db as fdb
os.environ["FEATHER_LOAD_THREADS"] = "8"
db = fdb.DB.open("memory.feather", dim=768)
# Cold start at 40K × 128-dim: ~1.7s vs 7.6s serial
At 8 threads, a 40K × 128-dim index drops from 7.6s to 1.7s (4.5×). The speedup saturates around 8–16 threads because the IO phase and cache contention become the bottleneck. But this was still a rebuild — every DB.open() call reconstructed the graph from the raw edge data. For a 500K-node index, that means 13.4s serial or 2.7s parallel. Every time.
v9: persisted HNSW graph
v9 ships with Feather DB v0.16.0. The change is direct: the HNSW graph is now embedded in the .feather file in its final in-memory representation. On load, Feather maps the persisted graph directly — no reconstruction, no pointer wiring, no invariant validation. The graph is ready to use as-is.
What "persisted graph" means structurally
In v3–v8, the graph section stored raw edge data: neighbor ID lists per node, level assignments, the global entry point. Reconstruction turned that into the in-memory graph structure. In v9, the persisted graph section stores the in-memory structure itself — including the exact link lists that HNSW traversal reads during search. On load, Feather reads the section and has a live graph without any build step.
Per-modality persist_graph flag
Persisting the graph is opt-in per modality. The v9 file header includes a per-modality persist_graph flag. When True, the full link list structure for that modality is embedded. When False, the modality falls back to the v8 behavior (parallel rebuild from edge data).
This matters because not every modality benefits equally. A modality with 5K vectors costs little to rebuild. A modality with 500K vectors is exactly where the 48ms vs 2.7s difference is felt.
int8 modalities get int8 graphs
For modalities with int8 quantization enabled, v9 persists the link lists in int8 form. The round-trip is exact: the int8 graph written at save() is byte-identical to the int8 graph used during search before the save. No precision is lost moving through the file boundary.
File size tradeoff
Persisting the link lists increases file size by approximately 25%. For a 500K-vector index at M=16, the embedded link lists add meaningful bytes — but the cold load benchmark makes the tradeoff obvious:
Cold load benchmarks across format versions
| Scenario | Load time | Speedup vs v8 serial |
|---|---|---|
v8 file, serial rebuild (FEATHER_LOAD_THREADS=1) | 13.4s | 1× |
v8 file, parallel rebuild (FEATHER_LOAD_THREADS=8) | 2.7s | 5× |
| v9 file, persisted graph load | 48ms | 279× faster than serial, 56× faster than parallel |
48ms versus 2.7s. The difference is whether your serverless function warms up before the request times out, whether your Kubernetes pod is ready before the liveness probe fails, whether your dev server feels instant or sluggish after a restart.
When the fast path does not apply
The 48ms load requires a v9 file with a persisted graph. Three conditions cause Feather to fall back to parallel rebuild instead:
forget()orpurge()called since the lastsave(). These mutate the graph. The persisted section is now stale. Feather marks the index dirty and rebuilds on the next load.- On-disk quantization enabled for the modality. When vectors are stored in quantized form on disk, the link list geometry may not match what the full-precision graph would produce. Feather skips graph persistence for these modalities and falls back to rebuild.
- The file is v3–v8. v9 readers load older files without issue — they just rebuild the graph as those versions always did.
The fix for the first case is straightforward: after any forget() or purge() batch, run compact() then save(). The compact() call defragments the graph after deletions, and the subsequent save() persists the clean graph at v9. The next DB.open() gets the 48ms path again.
import feather_db as fdb
db = fdb.DB.open("memory.feather", dim=768)
# Delete stale entries
db.forget(old_node_ids)
# Re-enable fast load path
db.compact()
db.save("memory.feather")
# Next open() will use the persisted v9 graph
Backward compatibility
v9 readers load v3–v8 files without modification. The section index design (introduced in v6) means the new persisted-graph section is simply absent in older files — the reader treats its absence as "fall back to rebuild." No migration tool required to read old files. Migration is only needed to get the new fast-load benefit, and it's a single save() call from a v0.16.0 install.
The format philosophy
Each format version solved a specific problem that emerged at scale. v6's section index solved discoverability. v8's parallel load solved the serverless cold start. v9's persisted graph solves the remaining gap: large indexes that were fast to query but slow to re-initialize.
The constraint — one file, zero infrastructure — has held across every version. The complexity lives in the format. The API stays the same: DB.open(), db.search(), db.save().
Install: pip install feather-db · GitHub: github.com/feather-store/feather