If an LLM builds your graph, Neo4j vs Memgraph is the wrong fight. Either one can do traversal. Neither should be your system of record.
I've moved Graphite Atlas's graph store twice. The first move was off Neo4j, because traversal latency hit us early. The second was to Memgraph running alongside Postgres. Between those two moves, a Memgraph snapshot and restart incident took production down, and we had to recover it by hand.
Atlas turns transcripts, SOPs, tickets and logs into typed process graphs. Every node and edge keeps its source span and a status, either confirmed or hypothesis. (Why I'm building it is its own post.) Once every edge carries evidence, the database has a different job. It isn't storing relationships anymore. It's storing claims about relationships.
Why we left Neo4j
Traversal latency hit us early. That was the whole reason, and it was enough.
Process graphs are full of multi-hop questions:
- Who sits on every path through this process?
- What does the full handoff chain look like from intake to close?
- What breaks if this system is retired?
These are deep walks across handoffs, and they're the questions Atlas exists to answer. When the core product question is slow, you don't have a tuning problem. You have an engine problem. So we went looking for an engine built around traversal speed.
Migrating from Neo4j to Memgraph: Cypher differences to check
We moved to Memgraph for its in-memory engine. The whole graph lives in RAM. Most of the port is Cypher, but don't assume Cypher is Cypher. Memgraph's Cypher differences page lists the ones that matter:
- Relationship uniqueness, existence and type constraints aren't supported. They throw a
SemanticException.
- Indexes aren't created automatically, and creating a constraint doesn't create an index.
- Neo4j-style
CREATE INDEX and REQUIRE DDL now parses, but constraint names are dropped with a warning.
If you lean on APOC or GDS, Memgraph's own blog flags those as the areas to audit.
Almost every Neo4j vs Memgraph comparison I found was written by a vendor, either Memgraph or one of its competitors. None of them cover what goes wrong after you migrate. Here's what went wrong for us.
The Memgraph outage: snapshots, restart and recovery
We had a snapshot and restart incident. It took production down, and we recovered it manually.
What changed afterward is the rest of this post. The graph store stopped being the only copy of anything.
If you run Memgraph in production, learn these behaviors from the storage docs before an incident teaches them to you:
- Snapshot creation needs a transactional view, so ingesting while a snapshot runs can spike memory.
- You can't load a snapshot created under a different storage mode.
- Recovery from snapshots on startup is on by default (
--data-recovery-on-startup). Your restart time and your snapshot health are the same problem.
The lesson I took: a store you can lose in a bad restart shouldn't hold anything you can't rebuild.
Why provenance text doesn't belong in RAM
RAM is the other thing to watch in any graph that carries text. Memgraph documents a baseline of about 204 bytes per vertex and 154 per edge before properties. The worked example on the same page uses 252 and 172, so measure your own data instead of trusting the formula. On top of that, every string property costs its length in bytes.
Provenance is text. Here's an illustration, not Atlas data. Put a 500-character source excerpt on every edge, and each edge costs more than three times its structural overhead. A million such edges is roughly 650MB of RAM before indexes and query memory.
The on-disk mode that would ease this is marked experimental in Memgraph's own docs. You can only switch to it on an empty database. In practice, you're RAM-bound.
What it costs: Neo4j vs Memgraph pricing
That RAM number matters twice, because Memgraph Enterprise is licensed by memory. Here's what the public numbers say:
| Neo4j | Memgraph |
|---|
| Free edition | Community: single instance. Clustering, online backup and RBAC are Enterprise. | Community. RBAC, audit log, multi-tenancy, CRON snapshots and automatic failover are Enterprise. |
| Self-managed paid | Enterprise, priced through sales | Enterprise from $25,000/yr for 16GB, metered by memory, no compute or replica fees |
| Managed | AuraDB Professional $65/GB/month. Business Critical $146/GB/month with a 99.95% SLA. | Not compared here |
| 16GB for a year | Professional ≈ $12,480. Business Critical ≈ $28,032. | $25,000 license, plus your own hardware |
Sources are Neo4j's pricing and operations manual, plus Memgraph's Enterprise docs and pricing post.
This isn't apples to apples. Aura includes the infrastructure. Memgraph's figure is a license, and you still pay for the machine. Self-managed Neo4j Enterprise has no public price. What the table does show is that both vendors gate access control, failover and audit behind the paid tier. Both also charge by memory, one per GB-hour and the other per license tier.
So the cheapest fix is to stop asking the graph engine to do those jobs. Every excerpt you keep in it is RAM you pay for twice: once in hardware, once in license or GB-hours.
Postgres as the system of record, Memgraph as a projection
Today Postgres and Memgraph work together in Atlas. Semantic search is the clearest example of what that buys us. Neither store could do it alone with speed or at scale. Recovery is mixed: some of it comes from rebuilds, some from backups.
Here's the split I'd use for any LLM-built graph.
Postgres owns the truth. That means documents, source spans, edge claims with review state and versions, tenancy enforced with row-level security, the audit trail, and pgvector embeddings. Done this way, the graph engine never needs the Enterprise-tier features from the table above.
The graph engine owns traversal. It's a derived projection. Nodes and edges carry IDs, types, status and a version, and no excerpts.
Writes go to Postgres first. You write an outbox row in the same transaction as the claim, so there's no dual write to drift:
insert into graph_outbox (tenant_id, entity_type, entity_id, version, op)
values ($1, 'edge', $2, $3, 'upsert');
A relay reads the outbox and upserts into Memgraph. Delivery is at-least-once, so the upsert has to be idempotent. This is where the missing relationship constraints bite. Memgraph won't stop a duplicate edge for you, so stable IDs and MERGE have to:
MERGE (a:Step {id: $src_id})
MERGE (b:Step {id: $dst_id})
MERGE (a)-[r:HANDS_OFF_TO {id: $edge_id}]->(b)
WITH r WHERE coalesce(r.version, 0) < $version
SET r.status = $status, r.version = $version
The version guard stops a replayed or out-of-order event from overwriting newer state. Create the index on :Step(id) yourself, because Memgraph won't.
With this pattern, losing the graph store costs you an outage, not data. A rebuildable projection also gives you a migration path. Change the projection, rebuild into a fresh instance, then swap.
Route by question: shape to the graph, truth to Postgres
Questions about shape go to the graph engine:
- Single points of failure. Which person or system sits on every path through a process?
- Handoff chains. Walk the full chain from intake to close.
- System-retirement impact. Find everything downstream of the thing you want to turn off.
Questions about truth go to Postgres:
- "Why does this edge exist?" Join the claim to its source span and show the transcript line.
- Diffs. What changed between two versions of a process?
- Evidence and review. List every hypothesis edge waiting on a human.
The best queries use both. Vector search in Postgres finds the entry points, the graph walks from there, and Postgres fills in the evidence. It's the same idea as faceted context: figure out where you are, then navigate.
This is why a pure labeled property graph isn't enough for this kind of data.
In a normal property graph, an edge is a fact. (Alice)-[:APPROVES]->(Refund) is true. When an LLM pulls that edge out of a transcript, it's a claim, and a claim carries more than a fact does:
- It has evidence, which is a span in a source document.
- It has a status: hypothesis until someone confirms it.
- It has a history. It can be confirmed, then contradicted by a newer SOP, then superseded.
- It can have rivals, because other documents may make competing claims.
You can cram all of that into relationship properties. Then you're doing document storage, versioning, review workflows and access control inside a traversal engine whose memory you pay for. I wrote about FHIR as a graph because clinical data really is a network. Graphs model relationships well. They don't model review and version lifecycles.
So the edge in the graph engine is the current projection of a claim. The claim, and everything that justifies it, lives in Postgres.
When to use a graph database vs Postgres
A pure property graph is enough when your edges are facts you own. You write them yourself, they need no evidence, you don't need per-tenant isolation, and you can lose the store and reload it from somewhere else.
Add Postgres as the system of record once your edges are claims. That means they come from a model, need a source span, need human review, or need versioned history and tenant isolation.
Use Postgres alone when traversals are shallow or occasional. A recursive CTE with the CYCLE clause (PostgreSQL 14+) handles a bounded walk:
with recursive downstream(dst_id, depth) as (
select dst_id, 1 from edge_claim
where src_id = $1 and status = 'confirmed'
union all
select e.dst_id, d.depth + 1
from edge_claim e join downstream d on e.src_id = d.dst_id
where e.status = 'confirmed' and d.depth < 6
) cycle dst_id set is_cycle using path
select distinct dst_id from downstream where not is_cycle;
If your team wants Cypher syntax, Apache AGE runs openCypher inside Postgres through a cypher() function. A query looks like select * from cypher('atlas', $$ MATCH (a:Step {id: 'intake'})-[:HANDS_OFF_TO*1..6]->(b) RETURN b $$) as (b agtype);, with the graph sitting next to your claims and your RLS policies.
Bring in a separate graph engine only when deep, frequent traversals are a product feature and users feel the latency. That's what happened to us on Neo4j. When you do add one, make sure you can delete it and rebuild it.
If you want to see this running on real process data, that's Graphite Atlas.