Paginating Deep Graph Traversals in Neo4j with APOC and LIMIT/SKIP
Learn how to paginate deep Neo4j graph traversals with APOC’s path expansion and LIMIT/SKIP to keep latency predictable and avoid out‑of‑memory errors.
25 Nov 2025, 02:48 UTC

Problem: Unbounded Traversals Spike Latency
When a Cypher query asks for all friends‑of‑friends up to five hops starting from a single user, Neo4j expands the frontier breadth‑first. In a densely connected social graph each hop can multiply the number of candidate nodes, producing millions of intermediate paths before any limit is applied. The result is unpredictable latency, sudden memory spikes, and occasional out‑of‑memory errors.
Thesis: Bounding Work with LIMIT and SKIP
By wrapping APOC’s path expansion procedures with SKIP and LIMIT (or using the built‑in pagination helpers) you force the planner to stop expanding after a fixed number of rows per request. The work per request becomes predictable, latency stabilises, and the heap usage stays within expected bounds.
How Neo4j Executes a Traversal
Neo4j’s execution engine first evaluates the apoc.path.expandConfig call, creating a frontier of nodes and relationships that satisfies the depth and filter criteria. Only after this expansion does it apply subsequent operators such as Order By, Skip and Limit. If the expansion is unbounded, the frontier can grow large before the pagination operators even see it.
Worked Example: Paginated Friends‑of‑Friends up to 5 Hops
Run the following statement in Neo4j Browser, Neo4j Desktop, or an Aura free instance. Ensure the APOC plugin is installed and enabled.
MATCH (start:User {userId: $userId})
CALL apoc.path.expandConfig(start, {
relationshipFilter: 'FRIEND>',
labelFilter: '+User',
minLevel: 1,
maxLevel: 5,
uniqueness: 'NODE_GLOBAL'
}) YIELD path
RETURN last(nodes(path)) AS friendOfFriend,
length(path) AS hops
ORDER BY friendOfFriend.userId
SKIP $skip
LIMIT $limit
Where:
$userIdis the identifier of the starting user.$skipand$limitare pagination parameters (e.g., 0 and 20 for the first page).- The
ORDER BYclause guarantees a stable order so that successive pages do not shift results.
To verify that pagination is applied early, prepend PROFILE to the query and inspect the plan output. You should see a Skip and Limit operator positioned after the PathExpansion node. The planner will also show the estimated number of database hits for the expansion step; with pagination enabled the hits reported for the expansion will be capped by the limit value plus the skip offset.
If you run the same query without SKIP and LIMIT, the profile will display a much larger number of hits and a higher memory allocation metric, illustrating the trade‑off.
Trade‑off and Limitations
- Planning overhead: Adding
SKIPandLIMITintroduces extra operators in the execution plan, which adds a few milliseconds of planning time. - Order stability: Without a deterministic
ORDER BY, the sameskip/limitpair can return different rows on subsequent calls as the underlying expansion order may vary. - Dense graphs: Even with pagination, a highly connected node can cause a large frontier before the limit is applied. Mitigate this by tightening the
relationshipFilterorlabelFilterinapoc.path.expandConfig(e.g., only followFRIENDrelationships of typeCLOSE).
Actionable Steps
- Enable APOC: place
apoc.confinconf/and setdbms.security.procedures.unrestricted=apoc.*(or use the APOC plugin UI in Neo4j Desktop). - Store the Cypher fragment above as a reusable parameterised query in your application layer.
- Run
PROFILEon a representative dataset (e.g., a copy of your production graph trimmed to a few hundred thousand nodes) and compare theDatabase HitsandMemory Allocationvalues between the paginated and non‑paginated versions. - Monitor heap usage via Neo4j metrics (
dbms.memory.heap.max,dbms.memory.heap.used) while load‑testing the paginated query to ensure it stays within expected bounds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.