🆕 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:

  1. Adds (or drains) StatefulSet replicas
  2. Enables / deallocates the new/removed Neo4j server in the cluster
  3. Waits for database reallocation to complete before marking the operation Successful

You verify the result using two complementary Cypher views:

Cypher ViewWhat It Shows
SHOW DATABASE <name>Database allocation status, primary/secondary count
SHOW SERVERSWhich 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

RequirementDetails
KubeDB installedOperator running in your cluster
Neo4j instancestatus.phase=Ready
kubectl accessWith 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 User nodes
  • 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 Running
  • appdb shows currentStatus: online
  • SHOW SERVERS shows 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: 2000 result confirms no data was lost during the scale-down. The database remained online and queryable throughout.


Understanding the Output

SHOW DATABASE Fields

FieldDescription
nameDatabase name
currentStatusonline means the database is healthy and accepting queries
currentPrimariesCountNumber of primary copies currently allocated
currentSecondariesCountNumber of secondary (read replica) copies

One row appears per server that hosts the database. Seeing "appdb", "online", 2, 0 twice means two servers each confirm they hold a primary copy.

SHOW SERVERS Fields

FieldDescription
namePod name (maps directly to Kubernetes pod)
stateEnabled = active cluster member
healthAvailable = healthy and reachable
hostingList 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