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 TRANSACTIONSand the query log include client user-agents; aneo4j-python/4.4in 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
MERGEhas a uniqueness (or key) constraint on the merge property. AMERGEwithout a backing index is a label scan per row and the source of most "the import got slower as it grew" tickets. - Zero
BTREEindexes. They were removed in Neo4j 5; any survivor means a 4.x migration was never completed. Recreate asRANGE,TEXT, orPOINTas appropriate. - No index with
state <> 'ONLINE'orpopulationPercent < 100. - Unused indexes:
readCount = 0andlastReadnull 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:
AllNodesScanorNodeByLabelScanat 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 Hitsrelative toRows: the pattern is traversing far more than it returns; reorder anchors so the most selectiveMATCHcomes 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.uuid → randomUUID(), apoc.coll.* → list comprehensions and reduce, apoc.periodic.iterate → CALL { } 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
customerIdproperty onOrderinstead 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
onlineon every allocated server; astore copyingthat 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 backupscheduled, 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, notbolt://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_sizeequalsserver.memory.heap.max_size(avoids resize pauses), and stays at or below 31g to keep compressed pointers.server.memory.pagecache.sizeis large enough to hold the store: compare with the sum of*.dbfile sizes underdata/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.timeoutanddb.memory.transaction.maxset, so one runaway query cannot take the server with it.db.logs.query.enabledon, 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
neo4jsuperuser 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_PLUGINSauto-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 usesSKIP/LIMITor 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.