Last week I needed to give my AI agent a memory that connects facts instead of just listing them. Not "the user likes Postgres," but "the user likes Postgres, worked with him at two companies, and every incident review he runs mentions connection pools." That is a graph. My first instinct was the one I always have: just use SQLite. And my second instinct, after two days of recursive CTEs, was to check what the HN front page was trying to tell me.
That same week, a Show HN called LatticeDB landed: an embedded, single-file property-graph database written in Zig, positioned as "like SQLite but for graph databases," with native HNSW vector search and BM25 full-text search in the same query layer. The marketing number going around is up to 2,819x faster graph traversal than SQLite. Numbers like that are usually a sign to keep scrolling. This time I did not. I read the benchmark methodology, then rebuilt the SQLite side myself and ran it on my own server.
What I found is more useful than either the hype or the dismissal: the gap is real, but it lives in one specific place. If your queries stay shallow, you will not see it. If they go deep, it is not a gap, it is a cliff.
What LatticeDB actually is
One file, no server, embedded in your process, ACID with a WAL. That is the SQLite part. The difference is what the file is organized for: SQLite arranges rows into tables, LatticeDB arranges nodes into a graph, and puts three indexes over the same node properties.
- Graph traversal with a Cypher subset: MATCH patterns, variable-length paths, MERGE, WITH, UNWIND, aggregations. No OPTIONAL MATCH or CALL procedures yet.
- Vector search as a native HNSW index, not an extension.
- Full-text search with BM25, tokenization, stemming, and fuzzy matching.
- Durable streams with a built-in graph changefeed: graph mutations come out as an ordered, replayable log from the same file, sharing the same transaction and WAL path as the writes.
Everything is queryable in one statement. From the README, this is the pitch in a single query: find chunks similar to an embedding, walk to their document, walk to the author.
MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query_vector < 0.3
AND doc.content @@ "neural networks"
RETURN doc.title, chunk.text, author.name
The same job in Postgres today means pgvector for embeddings, tsvector for text, and recursive CTEs for the joins, then gluing three result sets in application code. In one embedded file with no server, that combination is genuinely new. MIT license, Python and TypeScript and Go bindings, about 300 GitHub stars when I checked, so this is an early-stage project and I treated it that way.
Full disclosure: I have read the source and the benchmark harness carefully, but I have not yet shipped anything on LatticeDB. The SQLite numbers below are mine, run on my hardware. The LatticeDB numbers are quoted from its published benchmark and its own head-to-head comparison doc.
The benchmarks, and what is actually head to head
The LatticeDB vs SQLite comparison is the only one in their docs measured in the same harness on the same machine, over a social-network graph with a power-law degree distribution, and they publish the command to reproduce it. That honesty is why I took the rest seriously.
The headline table, 100K nodes and 500K edges, adjacency cache warm:
- 1-hop traversal: LatticeDB 8.0 microseconds vs SQLite 290.0 microseconds, 36x.
- 2-hop traversal: 38.7 microseconds vs 548.3 microseconds, 14x.
- 3-hop traversal: 197.3 microseconds vs 1.2 milliseconds, 6x.
- Variable path, depth 1 to 5: 134.4 microseconds vs 10.1 milliseconds, 75x.
Depth-limited traversal on a smaller 10K-node graph is where the eye-popping numbers live: 390x at depth 10, 713x at 15, 1,848x at 25, and 2,819x at depth 50, where SQLite needs 1.4 seconds and LatticeDB needs 500 microseconds. The docs themselves tell you how to read this: as "how much does depth cost you," not "LatticeDB is 3,000 times faster."
Elsewhere in the README, only the SQLite rows are head to head; the Neo4j and Kuzu numbers are third-party figures on hardware the authors do not control. Same caveat applies to the vector search table, where LatticeDB's 0.83 milliseconds for 10-nearest-neighbor over 1M vectors at 100 percent recall@10 competes with server databases like Weaviate and Qdrant that also pay network overhead, and beats sqlite-vec's brute-force 17 milliseconds by about 20x. Those are cross-benchmark comparisons, and the project says so. That kind of labeling is rare and it is the main reason I bothered rerunning anything at all.
I reran the SQLite side myself
The claims about LatticeDB are only as good as the SQLite side of the comparison, so I built my own version of it: a 100,000-node directed graph with 500,000 edges and a power-law in-degree distribution, the shape real social and citation graphs take. In-memory SQLite, one adjacency table, indexes on both columns, and the traversal written the way SQLite documentation actually recommends: a recursive CTE with UNION deduplication.
First attempt, I picked a random root node. It had 1 follower. Every traversal came back in microseconds, and for a moment the benchmarks looked like nonsense. Then I picked the most connected node, with 11,460 in-edges, and the real story appeared. Both runs are below, because the difference between them is the whole lesson.
From a random, low-degree node, everything is fast:
- 1-hop, indexed out-edges: 2.9 microseconds.
- 2-hop recursive CTE: 40.7 microseconds.
- Variable depth 1 to 5: 47.7 microseconds.
From the max-degree hub, the CTE cost explodes with depth:
- 1-hop, indexed out-edges: 956.7 microseconds for 11,460 rows.
- 1-hop via recursive CTE: 49.9 milliseconds.
- 2-hop recursive CTE: 305.0 milliseconds, touching 41,662 distinct nodes.
- 3-hop recursive CTE: 705.9 milliseconds, 86,034 nodes.
- Variable depth 1 to 5: 3.79 seconds, 98,936 of the 100,000 nodes in the graph.
My point lookups were never the problem: 2.2 microseconds for a primary-key hit, right in line with the roughly 0.2 microseconds LatticeDB reports for in-memory SQLite, and their docs admit the two engines are near-identical there. My server CPU is not an Apple M1, so do not compare my numbers to theirs row by row. Read the shape instead, because the shape is what transfers. At every depth, the recursive CTE costs explode as the frontier widens, each recursion level re-plans, and the UNION dedup compounds. That matches LatticeDB's published gap curve almost exactly, and it confirms the core mechanism behind their numbers: at depth, it is not that SQLite is slow, it is that per-level overhead is multiplied by frontier size, and frontier size in a power-law graph grows brutally.
Two honest caveats about my own test. The CTE ran per-level UNION deduplication; SQLite's CTE machinery is generic, while LatticeDB's BFS keeps a bitset of visited nodes and a warm adjacency cache, an apples-to-oranges specialization. And a hand-tuned application-level BFS in Python, batching the frontier with WHERE src IN (...) per level, would narrow the gap. It would not close it, because you would be re-implementing in application code what LatticeDB puts inside the engine next to the index. But I did not run that variant, so treat the 3.79 seconds as one honest measurement, not a ceiling or a floor.
The decision framework
After reading their docs and running my own numbers, here is the decision matrix I would actually use.
- Choose SQLite when your data is tabular. Sales records, sessions, event logs, user accounts. Their own comparison doc says it plainly: SQLite is the better general-purpose embedded database and will remain so. No argument here.
- Choose SQLite when several processes need the file. LatticeDB is single-writer and single-process. One process owns the file. SQLite in WAL mode handles many concurrent readers across processes gracefully.
- Choose SQLite when you need the ecosystem. GUI browsers, migration tooling, ORMs, hosted replicas, twenty-five years of Stack Overflow answers. LatticeDB has almost none of that yet, and at a few hundred stars it may never get all of it.
- Choose LatticeDB when relationships, semantics, and text collide in one query. The Cypher query above is the tell. If you currently glue pgvector, FTS5, and recursive CTEs together, or run a vector database plus a graph database plus a search index for one local workload, one engine that does all three natively is a real simplification, not a toy.
- Choose LatticeDB when traversal depth is the workload. Agent memory that follows multi-hop connections, Graph RAG, dependency and lineage graphs, recommendation neighborhoods. My own run showed a 3.79-second CTE query at depth 5; a 500-microsecond engine-side BFS is not an incremental win there, it is the difference between a feature you can ship and one you cannot.
- Choose a client-server database when you outgrow one machine. LatticeDB can stream its file's changes elsewhere continuously, but that is backup, not clustering. If many clients need to write over a network, that is Postgres or Neo4j territory.
The honest one-liner from their docs deserves repeating: SQLite is better for the general case, LatticeDB is better for the specific shape where relationships, semantics, and text all matter to the same query.
What this means for agent memory
This is why I went down this rabbit hole. My agent infra keeps per-user memory in SQLite today: a facts table, timestamps, full-text search via FTS5. It works, until the retrieval question becomes relational. "What do I know about this person connected to this project where the last interaction mentioned this library?" is three joins and a vector search away, and every hop costs a CTE recursion in a graph that keeps growing.
The changefeed idea is the sleeper feature here. If graph mutations come out as an ordered, replayable stream, then an embedding pipeline can react to new nodes without polling, and an audit log falls out for free since the stream shares the WAL path with the writes. My agent already writes an append-only audit trail, and getting that from the storage layer instead of maintaining it in application code is the kind of simplification I did not know I was shopping for.
But it is version 0.9.6 with a few hundred stars. I am not moving production memories this weekend. I am keeping an eye on the repo, and my plan is to prototype my agent memory on it in a side branch and see if the Cypher shape actually fits my queries. The 0.13 microsecond node lookup, the 0.83 millisecond vector search at 1M vectors, and that depth curve add up to something worth prototyping. None of it adds up to betting a product on a v0 database written in a language I cannot debug.
I write about databases, backend engineering, and AI infrastructure every week. Subscribe, it is free.
Have you hit the recursive CTE wall in SQLite, or are you running a graph database for agent memory already? What did you pick, and what did it cost you? I am genuinely torn between prototyping on LatticeDB and just living with FTS5 plus a hand-rolled adjacency cache, and I would like to hear from anyone who made either choice.
If you take one thing from this piece, make it this checklist:
- Shallow queries, tabular data, multi-process access: stay on SQLite, the gap never shows up.
- Deep traversals over power-law data: the CTE cost is quadratic in frontier size, plan for it now.
- Hybrid needs, one query: graph plus vector plus BM25 in one engine is the actual product, judge it on that, not the 2,819x.
- New single-maintainer v0 project: prototype on a branch, never in production, keep the export path open.
Top comments (0)