+1 (415) 649-9454

Neo4j Health Check Checklist (the 2026 Edition)

A Neo4j health check is the most common fixed-scope engagement we run, and the checklist behind it has changed a lot since we first published one. "HA cluster", BTREE indexes, USING PERIODIC COMMIT, db.indexes() — all gone. This is the 2026 edition: what a senior consultant actually looks at on a 5.26 LTS or 2025.x/2026.x deployment, in the order we look at it, with the commands to run yourself.

Work through it on a staging copy first if you can. Nothing here writes data, but PROFILE on a bad query can be heavy.

1. Version and support status

CALL dbms.components() YIELD name, versions, edition RETURN name, versions, edition;

Then answer, honestly:

  • Is the version supported? Neo4j 4.4 reached end of support on 30 November 2025. Neo4j 5.26 is the LTS release, supported until June 2028. Everything else in the 5.x line has rolled off; the CalVer line (2025.01 onwards, monthly) is supported on a rolling basis. See endoflife.date/neo4j and Neo4j's supported versions page.
  • Are the drivers current? SHOW TRANSACTIONS and the query log include client user-agents; a neo4j-python/4.4 in there means that application has not been touched in years. Driver 6.x is current.
  • Which Cypher version is the default? SHOW DATABASES YIELD name, defaultLanguage (2025.06+). A database still defaulting to Cypher 5 is fine, but it should be a decision, not an accident.

Unsupported version is a P1 finding on its own; the rest of the check is still worth doing, because it becomes the migration's pre-flight list. Our 90-day migration plan covers the path.

2. Constraints and indexes

SHOW CONSTRAINTS;
SHOW INDEXES YIELD name, type, state, populationPercent, labelsOrTypes, properties, lastRead, readCount
ORDER BY readCount;

What we look for:

  • Every label used in a MERGE has a uniqueness (or key) constraint on the merge property. A MERGE without a backing index is a label scan per row and the source of most "the import got slower as it grew" tickets.
  • Zero BTREE indexes. They were removed in Neo4j 5; any survivor means a 4.x migration was never completed. Recreate as RANGE, TEXT, or POINT as appropriate.
  • No index with state <> 'ONLINE' or populationPercent < 100.
  • Unused indexes: readCount = 0 and lastRead null over a meaningful window are candidates to drop; each one taxes every write.
  • Vector indexes (2026 addition): dimensions match the embedding model actually in use (1536 for text-embedding-3-small, 3072 for -large), similarity function is what the application assumes, and the index is not sitting on a property that was migrated to the native Vector type without being rebuilt.
  • Fulltext indexes exist for the properties your hybrid retrievers query, and their analyzer is appropriate for the language of the data.

3. Query performance

Pull the slowest statements from the query log (db.logs.query.enabled=INFO with db.logs.query.threshold=1s is a sane production setting), then PROFILE the top ten:

PROFILE
MATCH (c:Customer)-[:PLACED]->(o:Order)-[:CONTAINS]->(p:Product)
WHERE c.region = 'EMEA'
RETURN p.name, count(o) AS orders
ORDER BY orders DESC LIMIT 20;

Operators that are findings almost every time:

  • AllNodesScan or NodeByLabelScan at the start of a pattern that has a filtering property: missing index, or a property type mismatch (c.id = '42' against an integer property will never use the index).
  • CartesianProduct: a pattern with two disconnected parts, nearly always a bug.
  • Eager: the planner protecting you from a read/write conflict in the same statement, usually fixable by splitting the statement or reordering.
  • High DB Hits relative to Rows: the pattern is traversing far more than it returns; reorder anchors so the most selective MATCH comes first.

Also check for the deprecated constructs that still parse under Cypher 5 but will not under Cypher 25: exists(n.prop) (use n.prop IS NOT NULL), [:TYPE*1..3] where a quantified path pattern would let the planner do better, and any remaining apoc.* call that has a native replacement (apoc.create.uuidrandomUUID(), apoc.coll.* → list comprehensions and reduce, apoc.periodic.iterateCALL { } IN TRANSACTIONS). The deprecations page is the authority.

4. Data model review

Numbers first:

MATCH (n) RETURN labels(n) AS labels, count(*) AS nodes ORDER BY nodes DESC;
MATCH ()-[r]->() RETURN type(r) AS type, count(*) AS rels ORDER BY rels DESC;
// supernodes: the top degree nodes per label
MATCH (n) WITH n, count { (n)--() } AS degree
ORDER BY degree DESC LIMIT 20
RETURN labels(n), degree;

Then the judgement calls we flag:

  • Supernodes with degree in the millions that queries traverse through — usually a category or status node that should have been a property, or a relationship that needs a time bucket ((:Day) nodes) to fan out.
  • Properties that should be relationships: a customerId property on Order instead of (:Customer)-[:PLACED]->(:Order) is a relational schema wearing a graph costume.
  • Relationships that should be properties: (:Order)-[:HAS_STATUS]->(:Status {name:'SHIPPED'}) is a supernode factory.
  • Generic relationship types (RELATED_TO, LINK) with the real meaning in a property; the planner cannot use a property to choose an expansion.
  • Unlabelled nodes and labels with fewer than ten nodes.
  • Embeddings stored as LIST<FLOAT> on 2025.10+ when the native Vector type would halve the footprint (Cypher 25, driver 6.x required).

5. Cluster topology and disaster recovery

This section used to say "HA architecture". In 5.x and CalVer it is Autonomous Clustering: the cluster allocates each database's primaries and secondaries across servers according to a topology you declare.

SHOW SERVERS;
SHOW DATABASES YIELD name, requestedPrimariesCount, requestedSecondariesCount, currentStatus, role, address;

Checks:

  • Primaries: 3 for production writes (1 is a single point of failure; 2 cannot hold a majority). Secondaries as read scale demands.
  • Every database reports online on every allocated server; a store copying that never finishes is a disk or network finding.
  • Server placement spans availability zones (or hosts, on-prem); three primaries in one rack is not fault tolerance.
  • Backups: neo4j-admin database backup scheduled, stored off-cluster, and restored recently on a scratch server. An untested backup is a hope, not a plan.
  • Clients connect with neo4j:// routing URIs, not bolt:// to a single server, so failover is transparent; and read workloads use read routing so secondaries actually serve something.
  • Driver-side: retry logic via execute_query/managed transactions, not hand-rolled loops.

The clustering chapter of the operations manual is the reference if any of this is unfamiliar.

6. Memory and server configuration

neo4j-admin server memory-recommendation --memory=64g

Compare the recommendation with neo4j.conf:

  • server.memory.heap.initial_size equals server.memory.heap.max_size (avoids resize pauses), and stays at or below 31g to keep compressed pointers.
  • server.memory.pagecache.size is large enough to hold the store: compare with the sum of *.db file sizes under data/databases/<name>; a page cache smaller than the graph means every traversal risks disk I/O.
  • OS file descriptor limit at least 40,000; transparent huge pages disabled; swap off.
  • db.transaction.timeout and db.memory.transaction.max set, so one runaway query cannot take the server with it.
  • db.logs.query.enabled on, with a threshold — you cannot tune what you cannot see.
  • Metrics exported (Prometheus or CSV) and retained; page-cache hit ratio, transaction throughput, and GC pause time are the three charts we ask for first.

7. Security and operations

  • The neo4j superuser is not the application's login; applications use least-privilege roles (reader, a custom writer scoped to the labels it needs).
  • TLS on Bolt and HTTPS enabled; HTTP (7474) closed to the network on production.
  • SSO/OIDC configured where the organisation has it; no shared passwords in Compose files or CI variables.
  • Security event log enabled and shipped somewhere.
  • Plugins (APOC, GDS) pinned to versions that match the server line and upgraded with it; NEO4J_PLUGINS auto-download is for dev, not prod.

8. Application-side patterns

Finally, read the code:

  • Queries are parameterised ($id), not string-concatenated. Concatenation defeats the query cache and is an injection risk.
  • One driver instance per process, long-lived; sessions short-lived.
  • Batched writes (UNWIND $rows AS row MERGE ...), not one transaction per row.
  • Result sets bounded with LIMIT; pagination uses SKIP/LIMIT or keyset, not client-side slicing of full results.

What you get from us

When GraphGuru runs this checklist, the deliverable is a written report ordered by impact — each finding with the evidence (the plan, the config line, the count), the fix, and the effort — and a working session to walk your team through it. Most engagements take one to two weeks depending on graph size and the number of applications. If you would like the 2026 checklist run on your deployment, book a health check.