Sporadic Long-Running Queries During Batched Writes – What Could Be the Cause?

Hi everyone,

We’re developing a tool that uses Neo4j (Community Edition) to analyze workflows represented as graphs. To analyse the data, we first import the data into the Neo4j. The process consists of:

  1. Importing data from SQL databases into Neo4j (first ~10 queries)
  2. Preparation steps on the data (setting properties, aggregations, helper nodes/relationships)

Each import starts with an empty Neo4j instance. The full preparation phase includes ~400 Cypher queries executed in a fixed order. All queries:

  • Mutate the graph (i.e., write/update)
  • Use CALL { ... } IN TRANSACTIONS OF 5000 ROWS
  • Some use IN CONCURRENT TRANSACTIONS to speed things up

This usually completes in ~4 minutes. However, we’re encountering an intermittent issue:

Sometimes, one query (usually after query 300+) hangs for 1–2 hours, then completes.
However, if we cancel and re-run it (with the same data and parameters), it finishes in seconds.

Some context:

  • The problem is not tied to dataset size; even small datasets can trigger it.
  • The same dataset may work fine one time, and hang the next.
  • The problem only occurs after the SQL import phase, once all data is already in Neo4j. Thus, possible connection problems are not the cause.
  • The query that hangs varies, and doesn’t necessarily involve complex logic.
  • Not all queries that hang are using CONCURRENT TRANSACTIONS. Thus, I think it is not related to a deadlock.

We’ve noticed that adding a small delay between phases reduces the frequency of these issues. Even though we could do this, it would be more like a workaround then a solution.

Has anyone experienced similar symptoms?

  • What could be the reason for the occasionally long running queries.
  • What are the best practices for concurrency and batching in Community Edition?
  • Are there tuning strategies (other than switching to Enterprise) that help mitigate this?

Thanks for any insights you can offer!

I would suggest you provide more info:

  • versions of everything
  • if you know which query, an execution plan (EXPLAIN)

Yes, sorry forgot the versions:

  • Currently we use Neo4j 5.24.2, but we saw the same problem also with older versions
  • Unfortunately, it does not always hang on the same query. But as an example this is the query that hanged the last time:
MATCH (:`Dimension` {id: <id>})<-[:`PART_OF_DIMENSION`]-(caseId) 
WHERE exists((caseId)-[:`HAS_VIOLATIONS`]->()) 
WITH caseId 
CALL {
    WITH caseId 

    OPTIONAL MATCH (caseId)<-[:`PART_OF_CASE`]-()-[r1:`BEFORE`|`BEFORE_START` {suspended: true}]->()-[r2:`BEFORE` {reverse: true}]->() 
    SET r1.disarray = not r1.simplification, 
        r2.disarray = not r2.simplification 
    SET r1.violation = r1.disarray, r2.violation = r2.disarray

    WITH caseId 
    OPTIONAL MATCH (caseId)<-[:`PART_OF_CASE`]-()-[r1:`BEFORE` {reverse: true}]->()-[r2:`BEFORE`|`BEFORE_END` {suspended: true}]->() 
    SET r1.disarray = not r1.simplification, 
        r2.disarray = not r2.simplification 
    SET r1.violation = r1.disarray, r2.violation = r2.disarray
} 
IN TRANSACTIONS OF 5000 ROWS 
Cypher 5

Planner COST

Runtime SLOTTED

Runtime version 5.24

+---------------------+----+-------------------------------------------------------------------------------------------------+----------------+
| Operator            | Id | Details                                                                                         | Estimated Rows |
+---------------------+----+-------------------------------------------------------------------------------------------------+----------------+
| +ProduceResults     |  0 |                                                                                                 |            700 |
| |                   +----+-------------------------------------------------------------------------------------------------+----------------+
| +EmptyResult        |  1 |                                                                                                 |            700 |
| |                   +----+-------------------------------------------------------------------------------------------------+----------------+
| +TransactionForeach |  2 | IN TRANSACTIONS OF $autoint_1 ROWS ON ERROR FAIL                                                |            700 |
| |\                  +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      |  3 | r2.violation = r2.disarray                                                                      |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      |  4 | r1.violation = r1.disarray                                                                      |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Eager            |  5 | read/set conflict for property: disarray (Operator: 6 vs 3, and 3 more conflicting operators)   |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      |  6 | r2.disarray = NOT r2.simplification                                                             |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      |  7 | r1.disarray = NOT r1.simplification                                                             |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Eager            |  8 | read/set conflict for property: disarray (Operator: 6 vs 17, and 5 more conflicting operators)  |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Apply            |  9 |                                                                                                 |            700 |
| | |\                +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Optional       | 10 | caseId                                                                                          |            700 |
| | | |               +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Filter         | 11 | NOT r2 = r1 AND r2.suspended = true                                                             |              2 |
| | | |               +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Expand(All)    | 12 | (anon_10)-[r2:BEFORE|BEFORE_END]->(anon_11)                                                     |             31 |
| | | |               +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Filter         | 13 | r1.reverse = true                                                                               |             43 |
| | | |               +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Expand(All)    | 14 | (anon_9)-[r1:BEFORE]->(anon_10)                                                                 |            217 |
| | | |               +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Expand(All)    | 15 | (caseId)<-[anon_8:PART_OF_CASE]-(anon_9)                                                        |            322 |
| | | |               +----+-------------------------------------------------------------------------------------------------+----------------+
| | | +Argument       | 16 | caseId                                                                                          |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      | 17 | r2.violation = r2.disarray                                                                      |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      | 18 | r1.violation = r1.disarray                                                                      |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Eager            | 19 | read/set conflict for property: disarray (Operator: 21 vs 17, and 5 more conflicting operators) |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      | 20 | r2.disarray = NOT cache[r2.simplification]                                                      |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +SetProperty      | 21 | r1.disarray = NOT cache[r1.simplification]                                                      |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Optional         | 22 | caseId                                                                                          |            700 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +CacheProperties  | 23 | cache[r1.simplification], cache[r2.simplification]                                              |              2 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Filter           | 24 | NOT r2 = r1 AND r2.reverse = true                                                               |              2 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Expand(All)      | 25 | (anon_6)-[r2:BEFORE]->(anon_7)                                                                  |              8 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Filter           | 26 | r1.suspended = true                                                                             |             12 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Expand(All)      | 27 | (anon_5)-[r1:BEFORE|BEFORE_START]->(anon_6)                                                     |            234 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Expand(All)      | 28 | (caseId)<-[anon_4:PART_OF_CASE]-(anon_5)                                                        |            322 |
| | |                 +----+-------------------------------------------------------------------------------------------------+----------------+
| | +Argument         | 29 | caseId                                                                                          |            700 |
| |                   +----+-------------------------------------------------------------------------------------------------+----------------+
| +Filter             | 30 | getDegree((caseId)-[:HAS_VIOLATIONS]->()) > 0                                                   |            700 |
| |                   +----+-------------------------------------------------------------------------------------------------+----------------+
| +Expand(All)        | 31 | (anon_0)<-[anon_1:PART_OF_DIMENSION]-(caseId)                                     |            934 |
| |                   +----+-------------------------------------------------------------------------------------------------+----------------+
| +Filter             | 32 | anon_0.id = $autostring_0                                                                       |              1 |
| |                   +----+-------------------------------------------------------------------------------------------------+----------------+
| +NodeByLabelScan    | 33 | anon_0:Dimension                                                                                |             10 |
+---------------------+----+-------------------------------------------------------------------------------------------------+----------------+

Total database accesses: ?

  • The query execution is triggered via a Java program, with Spring Boot 3.3.4
  • The queries are created via the Cypher-DSL 2023.9.7 (we are not using Spring Data Neo4j)

Can you replace the two optional match with a union/union all to do the search first and apply the set ops after ?

I suppose in concurrent transactions, Neo4j has to put eager operators and you might encounter potential deadlocks. It depends on the degree node, Neo4j might need to lock the relations and the nodes before an update.

See in the logs if you find deadlock retries.

Or (maybe dumb, but i can't test it and it doesn't throw an error), remove the direction of the relationships, since it seems that's all those 2 statements are doing:

MATCH (:`Dimension` {id: <id>})<-[:`PART_OF_DIMENSION`]-(caseId) 
WHERE exists((caseId)-[:`HAS_VIOLATIONS`]->()) 
WITH caseId 
CALL {
    WITH caseId 

    MATCH (caseId)<-[:`PART_OF_CASE`]-()-[r1:`BEFORE`|`BEFORE_START` {suspended: true}]-()-[r2:`BEFORE` {reverse: true}]-() 
    SET r1.disarray = not r1.simplification, 
        r2.disarray = not r2.simplification 
    SET r1.violation = r1.disarray, r2.violation = r2.disarray
} 
IN TRANSACTIONS OF 5000 ROWS