
LanceDB vs SQLite-vec vs Chroma: Embedded Vector Storage Architecture & Setup for AI Agents
Evidence boundary (reviewed November 11, 2026): This is a version-specific architecture comparison with small, locally executed smoke tests, not a production-scale benchmark. We tested LanceDB 0.41.0, sqlite-vec 0.1.9 and Chroma 1.5.9 in isolated temporary storage. We did not measure throughput, p95 latency, maximum dataset size or memory footprint for a real agent workload. Hardware-independent migration thresholds are not claimed.
Which local vector storage system fits your agent?
Local coding agents sometimes need to store tool histories, retrieved snippets, or vectorized notes. The first architectural question is not “Which database is the fastest?” It is “Do I need one SQLite transaction with my application state, an analytical vector table, or a convenient collection interface?”
The three libraries offer different trade-offs:
- sqlite-vec: A SQLite extension that adds a
vec0virtual table and exact vector search to a database file. Attractive when application metadata is already in SQLite and transactional consistency matters. - LanceDB: A Lance-backed embedded table system that supports exact search and optional vector indexing. Attractive for columnar datasets, filters and index tuning.
- Chroma: A convenient embedded or server-based vector collection API with configurable indexing and embedding integration. Attractive for straightforward document-memory workflows, provided concurrent-writer and memory requirements fit the deployment.
A shared network service, distributed database or cloud vector search might be preferable once multiple machines or writers need robust access. These examples do not establish that one engine wins at a specific row count.
1. The tested package boundaries
The editorial smoke tests ran with Python 3.12, and the following pinned package versions:
lancedb==0.41.0sqlite-vec==0.1.9chromadb==1.5.9numpy==2.2.6for synthetic numeric data
These are the tested versions for this revision, not a prescription to freeze every production dependency forever. Install any required packages into a project-specific virtual environment, not the system interpreter. Some libraries have substantial dependencies; evaluate the deployment footprint separately.
2. LanceDB: a table-oriented vector workflow
LanceDB stores tables in Lance datasets and can perform vector searches with scalar filters. You can start without creating an approximate index, then test whether an index helps a representative dataset.
import lancedb
db = lancedb.connect("./agent_lance_store")
table = db.create_table(
"tool_events",
data=[
{"id": 1, "vector": [1.0, 0.0, 0.0], "failed": 1},
{"id": 2, "vector": [0.9, 0.1, 0.0], "failed": 1},
{"id": 3, "vector": [0.0, 1.0, 0.0], "failed": 0},
],
mode="overwrite",
)
rows = (
table.search([1.0, 0.0, 0.0])
.where("failed = 1", prefilter=True)
.limit(2)
.to_list()
)
assert len(rows) == 2
print([(x["id"], round(x["_distance"], 4)) for x in rows])
The mode="overwrite" argument is only for a disposable demonstration database. Do not apply it to a production table whose data must survive. For an existing live table, open it explicitly and use an intentional write/migration policy.
For this review, an additional 2,000-row synthetic example with 768-dimensional vectors, an exact search, a scalar prefilter and a top-five result returned successfully under LanceDB 0.41.0. That proves a basic API path works. It is not a measured latency or recall benchmark.
LanceDB’s Python table API documents search, where(..., prefilter=True) and to_list. The best index depends on dataset size, query metric and filtering selectivity, not a fixed “migrate at 250,000 vectors” rule.
3. sqlite-vec: vectors and metadata in one SQLite transaction
SQLite’s well-understood transaction model is attractive when tool execution events, session state and vector embeddings must be committed together. The sqlite-vec extension adds a vec0 virtual table inside the connection.
import sqlite3
import struct
import sqlite_vec
db = sqlite3.connect("./agent_state.db")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.enable_load_extension(False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA busy_timeout=5000")
db.execute(
"CREATE TABLE IF NOT EXISTS tool_logs("
"rowid INTEGER PRIMARY KEY, command TEXT NOT NULL)"
)
db.execute(
"CREATE VIRTUAL TABLE IF NOT EXISTS vec_logs USING "
"vec0(rowid INTEGER PRIMARY KEY, "
"embedding float[3] distance_metric=cosine)"
)
db.commit()
def f32(*values):
return struct.pack(str(len(values)) + "f", *values)
with db:
cursor = db.execute(
"INSERT INTO tool_logs(command) VALUES (?)",
("read-only-diagnostic",),
)
db.execute(
"INSERT INTO vec_logs(rowid, embedding) VALUES (?, ?)",
(cursor.lastrowid, f32(1.0, 0.0, 0.0)),
)
matches = db.execute(
"""
SELECT t.command, v.distance
FROM vec_logs AS v
JOIN tool_logs AS t ON v.rowid = t.rowid
WHERE v.embedding MATCH ? AND k = 3
ORDER BY v.distance
""",
(f32(1.0, 0.0, 0.0),),
).fetchall()
print(matches)
db.close()
The three-element vectors above are deliberately small so the distance can be checked by inspection. Real embedding dimensions and metrics must match the table declaration; do not mix output from different embedding models under one vector schema.
What we actually tested
In the isolated test with sqlite-vec 0.1.9, we created a normal SQLite table and vec0 table in the same file, enabled WAL journaling, inserted a corresponding row/vector pair and retrieved it through a nearest-neighbor join.
We also deliberately raised an exception inside a single transaction after writing both entries, then confirmed that neither entry remained. That establishes one concrete rollback behavior in this pinned environment, not a universal reliability guarantee for every extension version or deployment topology.
WAL permits ordinary SQLite readers and a writer to overlap in certain circumstances, but SQLite still has a single-writer constraint. busy_timeout can reduce transient lock errors; it does not make writes lock-free or eliminate every database-busy failure. For concurrent tools, use bounded writes and a sensible connection ownership model.
The sqlite-vec upstream repository notes that this is still a pre-1.0 project. Pin and validate its schema and transaction behavior during upgrades.
4. Chroma: collections without an implicit model download
Chroma makes it easy to create a persistent local collection. A common surprise is that query_texts may invoke an embedding model. Depending on the installed defaults and local cache, that can require downloading model data. This is not an entirely offline or dependency-free behavior.
For an offline numerical smoke test, provide precomputed embeddings and query vectors explicitly:
import chromadb
from chromadb.config import Settings
client = chromadb.PersistentClient(
path="./agent_chroma_store",
settings=Settings(anonymized_telemetry=False),
)
collection = client.get_or_create_collection(
name="project_notes",
configuration={"hnsw": {"space": "cosine"}},
embedding_function=None,
)
collection.upsert(
ids=["note_101", "note_102"],
documents=["GPU memory check", "Python context update"],
metadatas=[
{"subsystem": "docker"},
{"subsystem": "vllm"},
],
embeddings=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
)
result = collection.query(
query_embeddings=[[1.0, 0.0, 0.0]],
where={"subsystem": "docker"},
n_results=1,
)
assert result["ids"][0] == ["note_101"]
We executed a corresponding Chroma 1.5.9 local test, verified filtered query results and reopened the persistent collection to confirm the two records were saved. We also tested its newer configuration syntax rather than relying exclusively on the legacy metadata={"hnsw:space": ...} setting. This did not invoke a default embedding-model download.
For applications that truly need text embedding, supply a deliberate, reviewed embedding function and model. Confirm its license, network access, resources and storage requirements rather than letting an unexpected default run.
Chroma’s collection configuration reference describes the legacy and modern HNSW settings. Its embedded-system constraints caution against sharing one persistent storage path among independent writers. That operational limitation is distinct from a guarantee that every such deployment inevitably corrupts data.
5. How to select a storage engine without inventing a speed ranking
Use a consistent workload and compare the metrics that matter to your application:
- Transactional correctness: Can tool metadata and its corresponding vector be committed or rolled back as your design requires?
- Query semantics: Is search exact or approximate, and is the chosen distance metric consistent across engines?
- Concurrency: How many reader and writer processes will access a store, and which configurations are actually supported?
- Footprint: Include embedding-model weights, vector indices, metadata, page cache and background maintenance.
- Operational reliability: Test backup and recovery, version migration, table maintenance and permissions.
- Latency and quality: Only report p50/p95, recall@k or peak RSS once the same dataset, hardware and query suite have actually been measured.
The tests above show that each library can implement a small local retrieval task under the pinned versions. They do not demonstrate that any is the fastest, cheapest or most scalable choice.
Primary technical references
- LanceDB Python API
- Lance format vector indices
- sqlite-vec repository, versioning and compatibility
- Chroma collection configuration
- Chroma system constraints
- Chroma embedding-function behavior
Technical review: November 11, 2026. Experiments used small synthetic data and temporary local storage; no client secrets, production tool logs, external embedding downloads or complete lab package were published.