Backup & Restore Troubleshooting¶
Common backup and restore failures and their fixes. For the feature overview and configuration, see the Backup & Restore guide.
Tip
kubectl neo4j validate -f <manifest> catches a malformed Neo4jBackup before you apply
it, kubectl neo4j preflight -f <manifest> checks the cluster-side preconditions the
manifest depends on — the credentials Secret's keys, or the ServiceAccount's cloud-identity
annotation — and kubectl neo4j explain Neo4jBackup/<name> decodes the phase of one that
has already failed. See the CLI guide.
The credential checks below are written as kubectl run probes. preflight covers the shape of the same
problems without a probe pod; reach for a probe when you need to prove the credentials
actually work against the bucket, which preflight deliberately does not claim to do.
Common Backup Issues¶
Backup Job Failures¶
Symptom: Backup job fails to start¶
Diagnosis:
# Check backup resource status — status.phase tells you which stage it's in:
# Invalid → spec failed validation (message has the details)
# Waiting → target cluster missing or not Ready yet (transient)
# Pending → transient precondition (chain parent missing, chain busy,
# sharded preflight couldn't connect) — retried automatically
# Failed → terminal for one-shot backups
kubectl get neo4jbackup
kubectl describe neo4jbackup production-backup
# Check operator logs for backup controller errors
kubectl logs -n neo4j-operator-system -l control-plane=controller-manager --tail=-1 | grep -i backup
Common Causes & Solutions:
- Missing ServiceAccount: ```bash
The Neo4jBackup controller creates the backup ServiceAccount when it reconciles a backup¶
(the cluster controller does not) — check it exists.¶
(No Role/RoleBinding is created: the backup Job needs no Kubernetes API access.)¶
kubectl get serviceaccount neo4j-backup-sa
If missing, the Neo4jBackup has not been reconciled yet (or already reached¶
Completed/Failed, which are terminal). For a pending or scheduled backup, trigger¶
reconciliation with a no-op annotation change on the Neo4jBackup itself:¶
kubectl annotate neo4jbackup production-backup troubleshooting.neo4j.com/reconcile="$(date +%s)" --overwrite ```
-
Storage Configuration Issues:
-
Cluster Reference Problems:
Symptom: Backup job starts but fails during execution¶
Diagnosis:
# Check backup Job's Pod log (one-shot: <neo4jbackup-name>-backup;
# CronJob child: named by Kubernetes after the CronJob, <neo4jbackup-name>-backup-cron-<suffix>
# — find it with `kubectl get jobs`).
kubectl logs -n <ns> job/<job-name>
# Check Neo4j server logs for backup-related errors (which server
# was the leader at backup time).
kubectl logs <cluster>-server-0 -c neo4j | grep -i backup
Common Solutions:
-
Insufficient Disk Space (PVC storage):
Increase# `kubectl exec` into any server pod to inspect the bound PVC. kubectl exec <cluster>-server-0 -c neo4j -- df -h /dataNeo4jBackup.spec.storage.pvc.size(only effective when the operator provisions the PVC — see bring-your-own PVC otherwise). Note thatretention.maxCount/retention.maxAgedo not prune continuously — PVC retention runs only when the Neo4jBackup CR is deleted, so a long-lived scheduled CR accumulates artifacts until then. Prune manually (or via a recurringneo4j-admin backup aggregate) if the volume fills. -
Database Lock Issues:
# Check for long-running transactions kubectl exec production-cluster-server-0 -- cypher-shell -u neo4j -p password \ "SHOW TRANSACTIONS YIELD transactionId, currentQuery, elapsedTime WHERE elapsedTime > duration({seconds: 30})" # Solution: Wait for transactions to complete or schedule backups off-peak -
Memory Issues in Backup Process: Backup pod resources default to a Burstable profile (request 100m CPU / 512Mi, limit 1 CPU / 2Gi) but are tunable via
spec.options.resources(standardrequests/limits). Raise them for large databases;spec.options.pageCachecontrols the page-cache hint passed toneo4j-admin.
Cloud Storage Issues¶
S3 Backup Failures¶
Authentication Issues:
# Check AWS credentials using the backup service account (default: neo4j-backup-sa)
kubectl run backup-auth-check --rm -it --image=amazon/aws-cli --serviceaccount=<backup-serviceaccount> -- aws sts get-caller-identity
# Test S3 access
kubectl run backup-auth-check --rm -it --image=amazon/aws-cli --serviceaccount=<backup-serviceaccount> -- aws s3 ls s3://your-backup-bucket/
Solutions:
-
IAM Role Issues:
# Use IAM roles for service accounts (IRSA) on the Neo4jBackup CR. # The annotations land on the operator-managed neo4j-backup-sa. apiVersion: neo4j.neo4j.com/v1beta1 kind: Neo4jBackup spec: storage: type: s3 bucket: your-backup-bucket cloud: provider: aws identity: provider: aws autoCreate: annotations: eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/Neo4jBackupRole" -
Bucket Policy Problems:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789:role/Neo4jBackupRole" }, "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::your-backup-bucket", "arn:aws:s3:::your-backup-bucket/*" ] } ] } -
Identity conflicts between CRs — backups that worked start failing auth after another backup/restore CR was added:
neo4j-backup-sa/neo4j-restore-saare shared per namespace, and two CRs declaring differentidentity.autoCreate.annotationsoverwrite each other (last writer wins). Look for aServiceAccountAnnotationConflictWarning event (kubectl get events --field-selector reason=ServiceAccountAnnotationConflict). Fix: one identity per namespace with access to all backup locations, or split the CRs across namespaces.
Google Cloud Storage Issues¶
Service Account Problems:
# Check GCP credentials using the backup service account (default: neo4j-backup-sa)
kubectl run backup-auth-check --rm -it --image=google/cloud-sdk:slim --serviceaccount=<backup-serviceaccount> -- gcloud auth list
# Test GCS access
kubectl run backup-auth-check --rm -it --image=google/cloud-sdk:slim --serviceaccount=<backup-serviceaccount> -- gsutil ls gs://your-backup-bucket/
Solutions:
# Use Workload Identity on the Neo4jBackup CR
# (annotations land on the operator-managed neo4j-backup-sa)
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jBackup
spec:
storage:
type: gcs
bucket: your-backup-bucket
cloud:
provider: gcp
identity:
provider: gcp
autoCreate:
annotations:
iam.gke.io/gcp-service-account: "neo4j-backup@project.iam.gserviceaccount.com"
Azure Blob Storage Issues¶
Authentication Problems:
# Check Azure credentials using the backup service account (default: neo4j-backup-sa)
kubectl run backup-auth-check --rm -it --image=mcr.microsoft.com/azure-cli --serviceaccount=<backup-serviceaccount> -- az account show
# Test storage access
kubectl run backup-auth-check --rm -it --image=mcr.microsoft.com/azure-cli --serviceaccount=<backup-serviceaccount> -- az storage blob list --account-name storageaccount --container-name backups
Scheduled Backup Issues¶
Symptom: Scheduled backups not running¶
Diagnosis:
# Check CronJob status
kubectl get cronjob
kubectl describe cronjob production-backup-backup-cron # the operator names it <neo4jbackup-name>-backup-cron
# Check backup schedule configuration
kubectl get neo4jbackup production-backup -o yaml | grep -A 10 schedule
Common Solutions:
-
Invalid Cron Expression:
-
Timezone Issues: Schedules run in UTC — there is no
spec.timezonefield, andTZ=/CRON_TZ=prefixes in the schedule string are rejected by the validator (Kubernetes refuses timezone-embedded CronJob schedules). Convert your local time to UTC: -
Backup Window Conflicts:
Common Restore Issues¶
Restore Job Failures¶
Symptom: Restore job fails to start¶
Diagnosis:
# Check restore resource status
kubectl get neo4jrestore
kubectl describe neo4jrestore production-restore
# Check operator logs
kubectl logs -n neo4j-operator-system -l control-plane=controller-manager --tail=-1 | grep -i restore
Common Solutions:
-
Invalid Backup Reference:
A restore whose referenced backup has no Succeeded run yet sits in# Verify backup exists kubectl get neo4jbackup production-backup # Check backup completion status. One-shot backups reach phase "Completed"; # scheduled backups stay in "Scheduled" — check status.history for a # Succeeded run instead. kubectl get neo4jbackup production-backup -o jsonpath='{.status.phase}' kubectl get neo4jbackup production-backup -o jsonpath='{.status.history[*].status}'Pending("Waiting for backup to complete") and retries automatically — that is not an error. -
Target Cluster Issues:
-
Storage Access Problems:
Symptom: Restore job fails during execution¶
Diagnosis:
# The restore's status and events say which path it took and what failed
kubectl describe neo4jrestore production-restore
# Online restores (the default): the database's own status names the cause
kubectl exec target-cluster-server-0 -c neo4j -- cypher-shell -u neo4j -p <password> \
"SHOW DATABASE <name> YIELD name, currentStatus, statusMessage"
# Offline standalone restores only: the restore Job's log
kubectl logs job/production-restore-restore
Common Solutions:
-
Insufficient Storage Space:
-
Database Already Exists:
-
Version Incompatibility:
Symptom: Restore reports Failed with "server pods can't reach the seed source"¶
The Neo4jRestore message reads cluster "<name>"'s server pods can't reach the seed source: missing <credentials and/or endpoint settings>. The server JVM fetches the seed itself. Provide these on the cluster CR, or set annotation neo4j.com/auto-inherit-seed-creds="true" … (with standalone "<name>" on a standalone), and a SeedEndpointNotProjected Warning event carries the same text.
Cause: an online restore from cloud storage makes the Neo4j server pods fetch the seed, so they need the cloud credentials Secret projected via spec.extraEnvFrom (and, for MinIO / S3-compatible stores, the endpoint in spec.env).
Fix:
# On the Neo4jEnterpriseCluster or Neo4jEnterpriseStandalone CR:
spec:
extraEnvFrom:
- secretRef:
name: <your-backup-creds-secret>
Or, set the annotation neo4j.com/auto-inherit-seed-creds=true on the cluster or standalone CR — the operator patches extraEnvFrom (and the endpoint) automatically, which triggers one rolling restart so Neo4j picks up the env vars.
Symptom: Restore reports Failed with "the seed proxy could not merge the backup chain"¶
Cause: the restore seeds from a PVC backup whose artifact is (or may be) a differential, so the seed proxy merges its chain into one full backup first — and that failed. The message carries neo4j-admin's reason. Common causes:
- A link of the chain is gone ("Unable to find valid backup chain"): the full backup the differential builds on was deleted — by hand, or by retention pruning that removed a full whose differentials remained.
- The scratch space ran out ("cannot copy … to the scratch volume"): merging copies the chain first. Set
spec.options.tempStorageto at least the chain's size.
Fix: address the cause and re-trigger the restore by changing its spec, or restore from a run whose chain is complete.
Symptom: Standalone restore reports Failed with "cannot run against a running standalone (it restores offline: …)"¶
Cause: this standalone restore cannot run online — the message names why (a point-in-time restore that cannot run online, a source.type: storage path, cloud storage by pod identity on a standalone without spec.podServiceAccountAnnotations, a backup run with no recorded artifact) — so it runs neo4j-admin through a Job, which needs the instance stopped.
Fix: set spec.stopCluster: true. The operator stops the standalone for the restore and starts it again afterwards; every database on it is offline meanwhile.
Symptom: Restore stuck in Running, no Job created¶
Expected. Online restores — every cluster restore and most standalone ones — use Cypher (dbms.recreateDatabase or CREATE DATABASE OPTIONS{seedURI}); no Job is spawned. The restore's events show the seed URI it issued, and the database's status shows how seeding is going:
kubectl describe neo4jrestore <name> # "Online restore into …", "Online restore seed failed"
kubectl exec <server-pod> -c neo4j -- cypher-shell -u neo4j -p <password> \
"SHOW DATABASE <name> YIELD name, currentStatus, statusMessage"
If the status message is No seed providers found to satisfy the provided uri 's3://...', the server pods don't have the cloud creds projected — see the first symptom above. The restore fails once spec.timeout (default 5m) passes without the database coming online; raise it for large stores.
Symptom: Sharded DB restore rejected with "use Neo4jShardedDatabase.spec.replaceExisting"¶
Cause: Neo4jRestore doesn't support sharded databases — the Cypher shape (SET GRAPH SHARD / SET PROPERTY SHARDS) only fits CREATE DATABASE, not dbms.recreateDatabase.
Fix: restore via the Neo4jShardedDatabase CR's destructive-restore flow:
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jShardedDatabase
metadata:
name: products
spec:
# … existing sharding fields …
seedBackupRef: products-backup
replaceExisting: true
force: true
See Property Sharding for details.
Symptom: Sharded database Failed with "holds differential backups of …"¶
Cause: the Neo4jShardedDatabase's seedBackupRef names a PVC backup whose latest run holds a differential of one or more shards. From a PVC, Neo4j seeds a sharded database only from a full backup of every shard, and a differential cannot be merged into one. On a PVC, backupType: AUTO takes full backups of a sharded database, so this comes from an all-databases backup, or a sharded backup's run taken by an earlier operator version; status.history[].shardArtifacts[].type shows which shards are DIFF.
Fix: seed from a sharded backup's full run: let the next run of a spec.shardedDatabase backup land, or create a one-shot Neo4jBackup with spec.shardedDatabase and point seedBackupRef at it. The sharded database retries and seeds once a full run lands. To keep differentials, back sharded databases up to cloud storage, where they seed directly.
Point-in-Time Recovery (PITR) Issues¶
A Neo4jRestore with source.pointInTime runs online on CalVer, from cloud storage, into a database that does not exist yet. Otherwise a standalone runs the neo4j-admin database restore --restore-until=… Job, and a cluster refuses. source.type: pitr always takes the Job, so it is standalone-only. See Point-in-Time Recovery.
Symptom: "point-in-time restore into cluster … cannot run: …"¶
Cause: the cluster cannot restore this point in time online, and a cluster has no offline path. The message names why:
- the database exists — restore under a new name, then compare and copy back what was lost;
- Neo4j 5.26 — upgrade to CalVer, or use a
Neo4jDatabasewithspec.seedConfig.restoreUntilon CalVer; - a PVC backup — back up to cloud storage;
- no run started at or after the point in time — take a backup, or choose an earlier point.
Fix: address the cause and re-trigger the restore by changing its spec.
Symptom: PITR restore rejected for a cluster target¶
Fix: on a cluster use source.type: backup with source.pointInTime (above), or a Neo4jDatabase with spec.seedConfig.restoreUntil; source.type: pitr is for standalones.
Symptom: PITR restore fails with timestamp errors¶
Solutions:
-
Missing PITR source config:
source.type: pitrrequiressource.pitr.baseBackuporsource.pointInTime(or both):The operator rendersspec: source: type: pitr pointInTime: "2025-01-15T14:30:00Z" # ISO 8601 (metav1.Time) pitr: baseBackup: type: backup backupRef: production-backuppointInTimeintoneo4j-admin database restore --restore-until="2025-01-15 14:30:00". -
Timestamp Outside Backup Range:
-
Neo4j Version Compatibility:
Performance Issues¶
Slow Backup Performance¶
Diagnosis:
# Monitor backup progress
kubectl logs job/production-backup-latest -f
# Check resource utilization during backup
kubectl top pod production-cluster-server-0
Optimization Strategies:
-
Reduce primary load: Use database-specific backups and schedule during low-traffic windows.
-
Avoid overlapping backups: Stagger
Neo4jBackupschedules so only one job runs per cluster at a time. -
Storage Performance Tuning: Back the
Neo4jBackupJob's destination PVC with a high-performance storage class (e.g.fast-ssd) for backup staging. -
Network Optimization:
Slow Restore Performance¶
Optimization:
-
Target Cluster Resources:
-
Storage Configuration:
Monitoring and Alerting¶
Backup Health Monitoring¶
Prometheus Metrics (exported by the operator):
# Backup attempts, labelled by result ("success"/"failure")
neo4j_operator_backup_total{cluster_name, namespace, result}
# Backup duration histogram
neo4j_operator_backup_duration_seconds{cluster_name, namespace}
# Alert rules
groups:
- name: neo4j-backup
rules:
- alert: BackupFailure
expr: increase(neo4j_operator_backup_total{result="failure"}[24h]) > 0
labels:
severity: critical
annotations:
summary: "Neo4j backup failed"
description: "Backup for cluster {{ $labels.cluster_name }} failed"
Log Monitoring:
# Monitor backup logs
kubectl logs -f job/production-backup-latest | grep -E "(ERROR|WARN|SUCCESS)"
# Set up log alerts
kubectl logs -f -n neo4j-operator-system -l control-plane=controller-manager | \
grep -i "backup.*failed" --line-buffered | \
while read line; do
echo "BACKUP ALERT: $line"
# Send to alerting system
done
Backup Validation¶
Automated Validation Script:
#!/bin/bash
# Validate backup completeness
BACKUP_NAME="production-backup"
NAMESPACE="default"
validate_backup() {
# One-shot backups reach phase "Completed". Scheduled backups stay in
# "Scheduled" forever — check status.history for a Succeeded run instead.
local backup_status=$(kubectl get neo4jbackup $BACKUP_NAME -n $NAMESPACE -o jsonpath='{.status.phase}')
if [ "$backup_status" = "Scheduled" ]; then
local last_run=$(kubectl get neo4jbackup $BACKUP_NAME -n $NAMESPACE \
-o jsonpath='{.status.history[-1].status}')
if [ "$last_run" != "Succeeded" ]; then
echo "❌ Last scheduled run not successful: ${last_run:-no runs yet}"
return 1
fi
elif [ "$backup_status" != "Completed" ]; then
echo "❌ Backup failed or incomplete: $backup_status"
return 1
fi
# Backup duration of the latest successful run (the operator does not
# populate status.stats.size; check the .backup files in your storage backend
# for the artifact size)
local backup_duration=$(kubectl get neo4jbackup $BACKUP_NAME -n $NAMESPACE -o jsonpath='{.status.stats.duration}')
echo "ℹ️ Latest backup duration: ${backup_duration:-unknown}"
echo "✅ Backup validation passed"
return 0
}
# Run validation
validate_backup
Emergency Recovery¶
For full disaster recovery (corrupted primary, restore to a new cluster from latest backup), follow the standard restore flow in the Backup & Restore guide § Restore Operations. The normal Neo4jRestore CR with source.type: backup + instanceRef pointing at a fresh cluster IS the emergency procedure — there's no separate path. Use spec.options.replaceExisting: true to overwrite existing data. To roll back to a specific timestamp before the corruption, use a Neo4jRestore with source.type: backup, source.pointInTime and a new spec.database name: on CalVer, from cloud storage, it runs online on a cluster or a standalone; otherwise a standalone restores offline and a cluster fails, naming the reason. See Point-in-Time Recovery.
See Also¶
Cluster restore fails with "the seed FAILED: …"¶
Symptom: a cluster-target Neo4jRestore goes Failed with a message like:
Database "mydb" was created but the seed FAILED: Object not found at the
path: s3://… — fix the cause, DROP DATABASE mydb IF EXISTS, and re-trigger
the restore
The quoted part is Neo4j's own statusMessage — it names the real cause.
Most common cause: Object not found at the path: s3://… on an
S3-compatible endpoint (MinIO, on-prem) — the server JVMs are missing the
endpoint configuration. The cluster Cypher restore makes the server pods
fetch the seed, and the endpointURL field on the backup only reaches
backup/restore Job pods. The operator emits a SeedEndpointNotProjected
Warning event on the restore when it can verify the endpoint is missing.
Add to the cluster CR:
spec:
env:
- name: AWS_ENDPOINT_URL_S3
value: "http://minio.minio.svc:9000"
- name: JAVA_TOOL_OPTIONS
value: "-Daws.s3.forcePathStyle=true"
Recovery: fix the cause, DROP DATABASE <name> IF EXISTS (it holds no
data), then re-trigger the restore (see the next section).
Retrying a finished restore¶
Completed/Failed are terminal for the current spec generation. Any spec
change that bumps the generation issues a fresh attempt:
Restore stuck Pending on "Waiting for backup-seed-proxy"¶
Symptom: a PVC-backed cluster restore stays Pending with
Waiting for backup-seed-proxy Deployment to become Ready (…), then goes
Failed after the wait budget (3 minutes by default; spec.timeout
overrides) with the proxy pod's condition in the message.
Cause: the operator serves the backup PVC to the server pods through a small HTTP proxy Deployment, and that proxy can't start. The parenthesised diagnosis names the reason; the most common is the backup PVC being ReadWriteOnce and still attached to another pod/node (e.g. a backup Job pod that hasn't terminated, or a node that still holds the volume).
Fix: release the PVC (wait for / delete the pod holding it), or move the backups to RWX storage or object storage. Then re-trigger the restore with a spec bump.