🆕 New to KubeDB? Start with the quickstart guide before continuing here.
Neo4j Horizontal Scaling with KubeDB
Horizontally scaling a Neo4j cluster means adding or removing cluster members (pods) at runtime — without downtime and without data loss. KubeDB handles the orchestration automatically through Neo4jOpsRequest.
This guide walks you through:
- Seeding a user database with test data
- Scaling up from 3 → 5 members
- Scaling down from 5 → 4 members
- Verifying cluster topology and data integrity at each step
How It Works
KubeDB uses the Neo4jOpsRequest custom resource to perform horizontal scaling operations. Under the hood it:
- Adds (or drains) StatefulSet replicas
- Enables / deallocates the new/removed Neo4j server in the cluster
- Waits for database reallocation to complete before marking the operation
Successful
You verify the result using two complementary Cypher views:
| Cypher View | What It Shows |
|---|---|
SHOW DATABASE <name> | Database allocation status, primary/secondary count |
SHOW SERVERS | Which servers host which databases |
Tip (Neo4j 2025.x): Always use both views together for a reliable picture of cluster topology after a scale event.
Prerequisites
| Requirement | Details |
|---|---|
| KubeDB installed | Operator running in your cluster |
| Neo4j instance | status.phase=Ready |
kubectl access | With permissions to the demo namespace |
Step 1 — Set Up the Namespace
$ kubectl create ns demo
Step 2 — Deploy Neo4j
Apply the example manifest and wait for the cluster to become ready:
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.4.27/docs/examples/neo4j/quickstart/neo4j.yaml
$ kubectl get neo4j -n demo neo4j-test -w
Wait until STATUS shows Ready before proceeding.
Step 3 — Create a Database and Seed Data
Open a shell into pod neo4j-test-0 and run the following Cypher commands to:
- Create a new database called
appdb - Insert 2,000 test
Usernodes - Verify the count
# Retrieve the admin password
PASS=$(kubectl get secret -n demo neo4j-test-auth \
-o jsonpath='{.data.password}' | base64 -d)
# Create the database
$ kubectl exec -n demo neo4j-test-0 -- \
cypher-shell -u neo4j -p "$PASS" \
"CREATE DATABASE appdb IF NOT EXISTS WAIT"
# Seed 2,000 User nodes
$ kubectl exec -n demo neo4j-test-0 -- \
cypher-shell -d appdb -u neo4j -p "$PASS" \
"UNWIND range(1,2000) AS i CREATE (:User {id:i, name:'user-'+toString(i)})"
# Confirm the count
$ kubectl exec -n demo neo4j-test-0 -- \
cypher-shell -d appdb -u neo4j -p "$PASS" \
"MATCH (u:User) RETURN count(u) AS totalUsers"
Expected output:
totalUsers
2000
Step 4 — Scale Up (3 → 5 Members)
Apply the scale-up ops request:
apiVersion: ops.kubedb.com/v1alpha1
kind: Neo4jOpsRequest
metadata:
name: neo4j-horizontal-scale-up
namespace: demo
spec:
type: HorizontalScaling
databaseRef:
name: neo4j-test
horizontalScaling:
server: 5
reallocate:
strategy: "incremental"
batchSize: 1
$ cat <<'EOF' | kubectl apply -f -
apiVersion: ops.kubedb.com/v1alpha1
kind: Neo4jOpsRequest
metadata:
name: neo4j-horizontal-scale-up
namespace: demo
spec:
type: HorizontalScaling
databaseRef:
name: neo4j-test
horizontalScaling:
server: 5
reallocate:
strategy: "incremental"
batchSize: 1
EOF
neo4jopsrequest.ops.kubedb.com/neo4j-horizontal-scale-up created
$ kubectl wait \
--for=jsonpath='{.status.phase}'=Successful \
neo4jopsrequest/neo4j-horizontal-scale-up \
-n demo --timeout=900s
neo4jopsrequest.ops.kubedb.com/neo4j-horizontal-scale-up condition met
Verify the Scale-Up
Run these commands to confirm the cluster now has 5 members and that databases have been reallocated:
$ kubectl get neo4jopsrequest -n demo neo4j-horizontal-scale-up
NAME TYPE STATUS AGE
neo4j-horizontal-scale-up HorizontalScaling Successful 16s
$ kubectl get neo4j -n demo neo4j-test -o jsonpath='{.spec.replicas}{"\n"}'
5
$ kubectl get pods -n demo -l app.kubernetes.io/instance=neo4j-test
NAME READY STATUS RESTARTS AGE
neo4j-test-0 1/1 Running 0 ...
neo4j-test-1 1/1 Running 0 ...
neo4j-test-2 1/1 Running 0 ...
neo4j-test-3 1/1 Running 0 ...
neo4j-test-4 1/1 Running 0 ...
$ PASS=$(kubectl get secret -n demo neo4j-test-auth -o jsonpath='{.data.password}' | base64 -d)
$ kubectl exec -n demo neo4j-test-0 -- cypher-shell -u neo4j -p "$PASS" \
"SHOW DATABASE appdb YIELD name, currentStatus, currentPrimariesCount, currentSecondariesCount RETURN name, currentStatus, currentPrimariesCount, currentSecondariesCount"
name, currentStatus, currentPrimariesCount, currentSecondariesCount
"appdb", "online", 2, 0
"appdb", "online", 2, 0
$ kubectl exec -n demo neo4j-test-0 -- cypher-shell -u neo4j -p "$PASS" \
"SHOW SERVERS YIELD name, state, health, hosting RETURN name, state, health, hosting ORDER BY name"
name, state, health, hosting
"neo4j-test-0", "Enabled", "Available", ["neo4j", "system"]
"neo4j-test-1", "Enabled", "Available", ["neo4j", "system"]
"neo4j-test-2", "Enabled", "Available", ["appdb", "system"]
"neo4j-test-3", "Enabled", "Available", ["appdb", "system"]
"neo4j-test-4", "Enabled", "Available", ["system"]
What to look for:
- All 5 pods are
RunningappdbshowscurrentStatus: onlineSHOW SERVERSshows new servers hosting databases (reallocation is complete)
Step 5 — Scale Down (5 → 3 Members)
Apply the scale-down ops request:
apiVersion: ops.kubedb.com/v1alpha1
kind: Neo4jOpsRequest
metadata:
name: neo4j-horizontal-scale-down
namespace: demo
spec:
type: HorizontalScaling
databaseRef:
name: neo4j-test
horizontalScaling:
server: 3
reallocate:
strategy: "full"
$ cat <<'EOF' | kubectl apply -f -
apiVersion: ops.kubedb.com/v1alpha1
kind: Neo4jOpsRequest
metadata:
name: neo4j-horizontal-scale-down
namespace: demo
spec:
type: HorizontalScaling
databaseRef:
name: neo4j-test
horizontalScaling:
server: 3
reallocate:
strategy: "full"
EOF
neo4jopsrequest.ops.kubedb.com/neo4j-horizontal-scale-down created
$ kubectl wait \
--for=jsonpath='{.status.phase}'=Successful \
neo4jopsrequest/neo4j-horizontal-scale-down \
-n demo --timeout=900s
neo4jopsrequest.ops.kubedb.com/neo4j-horizontal-scale-down condition met
Verify the Scale-Down
$ kubectl get neo4jopsrequest -n demo neo4j-horizontal-scale-down
NAME TYPE STATUS AGE
neo4j-horizontal-scale-down HorizontalScaling Successful 37s
$ kubectl get neo4j -n demo neo4j-test -o jsonpath='{.spec.replicas}{"\n"}'
3
$ kubectl get pods -n demo -l app.kubernetes.io/instance=neo4j-test
NAME READY STATUS RESTARTS AGE
neo4j-test-0 1/1 Running 0 ...
neo4j-test-1 1/1 Running 0 ...
neo4j-test-2 1/1 Running 0 ...
$ PASS=$(kubectl get secret -n demo neo4j-test-auth -o jsonpath='{.data.password}' | base64 -d)
$ kubectl exec -n demo neo4j-test-0 -- \
cypher-shell -d appdb -u neo4j -p "$PASS" \
"MATCH (u:User) RETURN count(u) AS totalUsers"
totalUsers
2000
$ kubectl exec -n demo neo4j-test-0 -- cypher-shell -u neo4j -p "$PASS" \
"SHOW DATABASE appdb YIELD name, currentStatus, currentPrimariesCount, currentSecondariesCount RETURN name, currentStatus, currentPrimariesCount, currentSecondariesCount"
name, currentStatus, currentPrimariesCount, currentSecondariesCount
"appdb", "online", 2, 0
"appdb", "online", 2, 0
$ kubectl exec -n demo neo4j-test-0 -- cypher-shell -u neo4j -p "$PASS" \
"SHOW SERVERS YIELD name, state, health, hosting RETURN name, state, health, hosting ORDER BY name"
name, state, health, hosting
"neo4j-test-0", "Enabled", "Available", ["neo4j", "system"]
"neo4j-test-1", "Enabled", "Available", ["appdb", "neo4j", "system"]
"neo4j-test-2", "Enabled", "Available", ["appdb", "system"]
✅ The
totalUsers: 2000result confirms no data was lost during the scale-down. The database remained online and queryable throughout.
Understanding the Output
SHOW DATABASE Fields
| Field | Description |
|---|---|
name | Database name |
currentStatus | online means the database is healthy and accepting queries |
currentPrimariesCount | Number of primary copies currently allocated |
currentSecondariesCount | Number of secondary (read replica) copies |
One row appears per server that hosts the database. Seeing
"appdb", "online", 2, 0twice means two servers each confirm they hold a primary copy.
SHOW SERVERS Fields
| Field | Description |
|---|---|
name | Pod name (maps directly to Kubernetes pod) |
state | Enabled = active cluster member |
health | Available = healthy and reachable |
hosting | List of databases currently allocated to this server |
Troubleshooting
If this OpsRequest does not finish, first inspect the affected pod and then check the kubedb-ops-manager operator logs for the exact error. For a shared checklist, see the Neo4j Ops Request Overview.
Ops request stuck in Progressing
Check the OpsRequest status, the pod events, and cluster allocation:
$ kubectl describe neo4jopsrequest -n demo <ops-request-name>
$ kubectl get pods -n demo -l app.kubernetes.io/instance=neo4j-test
$ kubectl describe pod -n demo neo4j-test-0
If the node is full, the new server count may not be schedulable. Check node capacity:
$ kubectl describe node <node-name> | grep -A 10 "Allocated resources"
Database shows offline after scaling
Neo4j may need time to reallocate. Wait a few seconds and re-run SHOW DATABASE. If it persists, check the kubedb-ops-manager logs and pod readiness:
$ kubectl logs -n <kubedb-namespace> -l app.kubernetes.io/name=kubedb-ops-manager --tail=50
$ kubectl get pods -n demo -l app.kubernetes.io/instance=neo4j-test
OpsRequest moves to Failed
Read the failure condition and then inspect the kubedb-ops-manager logs:
$ kubectl get neo4jopsrequest -n demo <ops-request-name> -o jsonpath='{.status.conditions}' | jq .
$ kubectl logs -n <kubedb-namespace> -l app.kubernetes.io/name=kubedb-ops-manager --tail=50
Cleanup
Remove all resources created in this guide:
$ kubectl delete neo4jopsrequest -n demo \
neo4j-horizontal-scale-up \
neo4j-horizontal-scale-down
$ kubectl delete neo4j -n demo neo4j-test
$ kubectl delete ns demo
Next Steps
- Neo4j Vertical Scaling — adjust CPU and memory
- Neo4j TLS Configuration — enable encrypted connections































