FalkorDB on our own dependency graph: five questions, three engines, and a load that lied twice
Published
Our real dependency graph (100,037 declared rows) into FalkorDB and two Postgres servers. All five answers matched; the fastest engine was whichever one sat next to the client, and the bulk load duplicated nodes first.
FalkorDB calls itself an ultra-fast graph database, built for exceptionally low latency. It stores its adjacency matrix as sparse matrices, which it calls a first for a queryable property graph store. We have a graph of our own: the dependency manifests we read from 3,105 repositories, 99,251 distinct repository-to-package edges.
We asked the same five questions the site already answers with SQL. We ran them on the graph engine and on two Postgres servers holding the same rows: the production database behind a Supabase pooler in Singapore, and a copy on this Mac. Fifteen runs each, after three warm-ups.
1.Result
| Graph question | Rows | Postgres over the pooler | FalkorDB | Postgres next door |
|---|---|---|---|---|
| Top packages across 200 repos | 20 | 142.7 ms | 22.3 ms | 40.2 ms |
| Neighbours sharing eight packages or more | 20 | 66.0 ms | 6.0 ms | 18.0 ms |
| Repos that declare typescript | 1 | 36.8 ms | 2.0 ms | 1.1 ms |
| Repos that depend on microsoft/TypeScript | 1 | 36.5 ms | 1.3 ms | 1.1 ms |
| Other packages those neighbours declare | 20 | 125.7 ms | 15.2 ms | 24.9 ms |
Top packages across 200 repos
142.7 ms
Postgres over the Singapore pooler: 22.3 ms on the graph, 40.2 ms local
Neighbours sharing eight packages or more
6.0 ms
The traversal win that survives the geography: 18.0 ms on the local Postgres
The same load, indexed first
39 s
99,251 edges, against 498 s with no index on the merge keys
Every one of the five answers came back identical, row for row and in the same order, on all three engines. Against the remote Postgres that is a 6x to 28x gap.
Against the Postgres on the same desk the gap is 1.1x to 3.5x, and the two cheapest questions are a tie. Those local ratios use the second FalkorDB run. On the first run the 1 ms count reads as the graph losing by 0.55x. That is the floor of a 1 ms measurement, not a different answer.
2.Most of the win is geography
The two trivial questions are the tell: one indexed count costs 36.8 ms on the pooler and 1.1 ms next door, for the same plan and the same 1,114 rows. That 35 ms is the round trip to Singapore, not graph algebra. Put the SQL where the client is and the engine difference nearly disappears.
What survives that correction is traversal. Looking for neighbours that share eight or more packages runs 3.5x faster on the graph, and the two-hop "what else do those neighbours declare" runs 1.7x faster. The aggregate over 200 repos is 1.8x, so even the GROUP BY our SQL does over 100k rows is not where the money is.
3.What the graph cost to load
| Load | Edges / rows | Time | Duplicate nodes |
|---|---|---|---|
| FalkorDB, no index on the merge keys | 99,350 edges | 498 s | 27 repo ids |
| FalkorDB, indexes created first | 99,251 edges | 39 s | none |
| Postgres, COPY plus the four indexes | 100,037 rows | not timed in this pass | n/a |
An index on the two keys we merge on made the identical load 12x faster and stopped it inventing edges: 99,350 became exactly the 99,251 pairs in the source. The load is the price Postgres never charges us, because the SQL side already had the rows.
4.Two ways our own loader lied
The first shape we tried was one MERGE clause per edge, a few hundred per query. FalkorDB answered with a redeclared-variable error, and after we gave every clause its own variable name the container died instead, exit code 139, twice. One UNWIND row with a single MERGE pattern loaded the same data without incident.
The second was quieter. Merging an anonymous node inside FOREACH creates a duplicate: a node for repository id 2 was created again even though id 2 already existed. Our graph ended up with 27 duplicate ids, 19 extra nodes and 99 extra edges, and the typescript count answered 1,118 where SQL answered 1,114. Named variables in a separate pass fixed it.
Two more mistakes were ours, not the engine’s. The first measurement compared a snapshot taken while the writer was still running (920 against 647 on one count). The Used-by traversal was first written with the arrow pointing the wrong way and returned 0. Both are in the raw rows.
Decision
- Measure the network first
- A local Postgres answers the same five questions 1.1x to 3.5x off the graph, not 28x.
- Most of that headline is a 35 ms round trip to Singapore
- Move the traversal, not the warehouse
- The only durable win is deep path following.
- 6.0 ms against 18.0 ms for shared-stack neighbours
- 15.2 ms against 24.9 ms two hops out
- Create the indexes before the load
- The same 99,251 edges took 498 s with no index on the merge keys and 39 s with one.
- The indexed run also stopped producing duplicate nodes
- Do not batch MERGE clauses
- Hundreds of MERGE clauses in one query took the FalkorDB container down twice (exit 139).
- One UNWIND row per batch loaded it fine
What we did not measure
- FalkorDB’s actual selling point: every query here is a plain OpenCypher pattern match, the same shape we would write on any Cypher engine. Sparse-matrix linear algebra was never exercised.
- A graph that stays the same size: our pipeline rewrites repo_dependencies every cycle. The table was empty when this task started, so the whole graph was rebuilt, and this is one day’s snapshot.
- Concurrency: one client, 15 sequential runs per question. No statement about throughput under load is supported by this.
- The remote Postgres p95: it reached 190 ms and 239 ms on two questions. The client and the pooler share this 8 GiB machine, so treat the remote p95 as noise.
- FalkorDB Cloud or a tuned instance: this is the stock Docker image with its default 1,000 ms query timeout, on the same laptop as everything else.
Raw rows, including the crash sequence, the duplicate-node numbers and our own two mistakes: docs/analysis/2026-10-07-falkordb-against-our-own-dependency-graph.jsonl. This is our measurement of the claim, not a score for the tool.