Every agent that runs longer than one conversation needs memory it can search by meaning, not just by key. "What did this user tell me about their deploy setup three weeks ago?" is a semantic query, and answering it means embeddings and a vector store. The question isn't whether — it's which, and the three that keep coming up for builders are sqlite-vec, LanceDB, and Qdrant.
They're usually pitched as a speed-and-recall bake-off. That's the wrong axis. The useful way to tell them apart is operational shape: sqlite-vec puts your memory in a file, LanceDB puts it in a library, and Qdrant puts it in a service. Pick the shape that matches how big your memory is and how many things need to read it — the rest follows.
sqlite-vec: memory in a file#
sqlite-vec is a pure-C SQLite extension (~7.9k stars, backed by Mozilla Builders). You load it into SQLite and get a vec0 virtual table. The payoff is that your agent's semantic memory lives in the same .db file as everything else — conversation log, preferences, task state — and you query it all in one statement:
CREATE VIRTUAL TABLE vec_memories USING vec0(
memory_id INTEGER PRIMARY KEY,
user_id INTEGER, -- metadata column, filterable in WHERE
embedding FLOAT[384], -- the vector
+content TEXT -- auxiliary column, stored alongside
);
-- semantic recall + a SQL filter, in one query
SELECT memory_id, content, distance
FROM vec_memories
WHERE embedding MATCH :query_vec
AND user_id = 42
AND k = 5
ORDER BY distance;
The honest limitation: sqlite-vec does brute-force KNN only. There's no ANN index yet — it's an open roadmap item — so every query is a linear scan. That's genuinely fast enough up to roughly a few hundred thousand vectors, and at 1M+ or high-dimensional embeddings it starts taking seconds. It's also still pre-1.0 (v0.1.x), so expect breaking changes.
Reach for it when your agent is local-first, single-user, or runs at the edge, and you want zero infrastructure — one file you can back up by copying it. This is when a single SQLite file is genuinely enough, and for most solo builders it is.
LanceDB: memory in a library#
LanceDB (~11k stars) is also embedded — no server to run — but built for scale sqlite-vec can't reach. It stores data in the Lance columnar format on disk and runs in-process. Two things make it more than "sqlite-vec but bigger."
First, real ANN indexes. Search is exact by default, and once a table grows you build an index — IVF_PQ, or HNSW-backed variants like IVF_HNSW_SQ — trading a little recall for order-of-magnitude speed. That's the brute-force-vs-approximate tradeoff, made explicit and on your terms.
import lancedb
db = lancedb.connect("./agent_memory.lance") # embedded, on-disk
tbl = db.create_table("memories", data=[
{"vector": embed("prefers dark mode"), "user_id": 42, "text": "prefers dark mode"},
])
# tbl.create_index(metric="cosine", index_type="IVF_PQ") # once the table is large
hits = (tbl.search(embed("what UI settings?"))
.where("user_id = 42") # SQL-style metadata filter
.limit(5).to_pandas())
Second, automatic versioning. LanceDB records zero-copy versions of a dataset as it changes, so you can "time-travel" to the memory as it existed at any past point — with no snapshot system of your own. For agents that's not a nicety; it's auditability. You can replay exactly what the agent knew when it made a decision, or roll back a poisoned batch of memories. LanceDB is also pre-1.0 (0.3x), with Python and Node SDKs versioned separately.
Reach for it when you've outgrown a single file but still don't want to operate a daemon — millions to 100M vectors, in-process, on local disk or object storage — and especially when you want versioned, replayable memory.
Qdrant: memory in a service#
Qdrant (~33.6k stars, Rust) is the only one of the three past 1.0 — v1.18, shipped May 2026. It's a standalone server: you run it (docker run -p 6333:6333 qdrant/qdrant) or use Qdrant Cloud, and talk to it over the network. That's the cost; the capabilities are the reason.
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.upsert("memories", points=[
models.PointStruct(id=1, vector=embed("prefers dark mode"),
payload={"user_id": 42, "text": "prefers dark mode"}),
])
hits = client.query_points("memories", query=embed("what UI settings?"),
query_filter=models.Filter(must=[
models.FieldCondition(key="user_id", match=models.MatchValue(value=42))]),
limit=5).points
Qdrant's edge is at scale and in filtering: HNSW ANN with rich JSON payload filters — keyword, full-text, numeric range, geo, boolean logic — woven into the graph traversal rather than bolted on after. Add quantization (v1.18's TurboQuant cuts RAM hard) plus sharding, replication, and RBAC, and it scales to billions of vectors as a memory service many agents share.
A caveat builders trip on: the Python client's local mode (QdrantClient(":memory:")) is real but capped at roughly 20,000 points — it's for dev and tests, not production. For on-device, that's what Qdrant Edge is for; for anything real, run the server.
Reach for it when memory is shared infrastructure — many agents, many users, one store — and you need production filtering, RAM control, and ops.
The one-line decision#
Same as it ever was with storage: don't run a server until you have to.
- sqlite-vec until it hurts — local, single-user, one file, zero infra, up to a few hundred thousand memories.
- LanceDB when you want to stay embedded but need ANN, scale, or versioned/replayable memory.
- Qdrant when memory becomes a shared service that needs heavy filtering, huge scale, and real ops.
Most agents start in a file and never leave it. Build so the memory layer is a swap, not a rewrite — a thin remember() / recall() interface over whichever of these you're on — and moving from a file to a service becomes a migration you do when the data forces it, not a bet you have to place on day one. If you're still choosing embeddings first, that's its own decision — get it right before the store, because re-embedding a million memories is the expensive part.



