Skip to main content
When a change arrives, the engine has to answer one question: which documents embed this thing, and therefore need recomputing? Getting that set right — not too big, not too small — is what makes the engine both correct and fast.

The two directions of fan-out

A change can be to the primary itself or to something it embeds.
  • Primary change (an orders row, a Product node): recompute that one document. Easy.
  • Embedded change (a customer the order references, a Region two hops from the product): recompute every primary that embeds it. This is the fan-out, and it can touch many documents from one change.
For Postgres, the engine keeps an in-memory reverse index of foreign keys to answer this. For Neo4j, it runs a Cypher query anchored on the changed element’s ID, walking back to the affected primaries along the spec’s declared paths.

Bounded by the spec

Fan-out only follows paths the spec declares, up to fan_out_max_hops. A node whose label the spec never traverses produces zero recomputations — the change event is read and discarded. This is what keeps a single change from rippling across the whole graph. Changing a shared lookup node must recompute every primary document that embeds that value. This can be expensive, but it is required for the target documents to converge.

The hot-endpoint problem

Now consider a shared lookup node — like a Category that thousands of products point at. In the validation graph there was a single shared status node referenced by ~99,000 documents through one relationship type. When a single document’s edge to that shared node changes, the naive fan-out anchors on the relationship’s endpoints — including the shared node — and asks “which documents reach this node?” The answer is all 99,000, even though only one document’s edge actually changed. Without handling, a single edge change triggered a 59,498-document recompute in testing. A 1,000-operation burst queued ~5,800 such cascades — hours of wasted work for changes that should each touch one document.

Hot-endpoint detection

At startup, the engine probes each projection path’s leaf cardinality. Any endpoint label below a threshold (default 100, via VS_NEO4J_HOT_NODE_THRESHOLD) is flagged “hot” and its element IDs are captured in a small in-memory set. At event time, relationship events involving a hot endpoint have that endpoint filtered out of the fan-out anchor set — so only the primary-side key reaches the recompute path:
With hot-endpoint detection, the relationship event keeps only the primary-side anchor and recomputes the document whose edge changed.

What is not filtered

The filter applies to relationship events only. A node-property change on a hot node — say, renaming that shared status node from “Published” to “Live” — still cascades to all ~99,000 documents, because the embedded value genuinely changed for all of them. That’s correct: The filter removes spurious fan-out (an edge change masquerading as a graph-wide change), never legitimate fan-out (a shared value actually changing).

Cost

  • Startup: one count query for each analysable projection-path prefix.
  • Memory: a set of detected endpoint IDs, keyed by relationship type.
  • Per event: relationship-type and endpoint membership lookups.
Set VS_NEO4J_HOT_NODE_THRESHOLD=0 to disable detection entirely (the old behavior). Raise it if a genuine fan-out target sits just above the default and you’d rather treat it as hot.