Argo CD Sync Fails: Hook Job BackoffLimitExceeded Blocks GitOps Deploy
Fix Argo CD syncs that hang or fail when a PreSync or PostSync hook Job hits BackoffLimitExceeded: diagnose the failing Job, its pod logs, and the hook policy.
- #automation
- #devops
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
Argo CD runs resource hooks — PreSync, Sync, and PostSync Jobs — as part of a sync operation. When a hook Job’s pods fail repeatedly and the Job exhausts its backoffLimit, the Job reports BackoffLimitExceeded, the sync is marked failed or stuck in Progressing, and the whole GitOps rollout halts:
Job.batch "db-migrate-presync" is in failed state: BackoffLimitExceeded
one or more synchronization tasks are not valid
Operation has failed: hook job db-migrate-presync failed
In the Argo CD UI the Application shows OutOfSync/Failed with the hook resource red, and the sync will not advance to the application manifests until the hook succeeds or is removed.
Symptoms
- Argo CD sync stops at a
PreSync/PostSynchook and never reaches the app resources. - A hook
JobshowsBackoffLimitExceededinkubectl get jobandFailedconditions. - The hook’s pods are in
ErrororCrashLoopBackOff, or wereOOMKilled. - The Application status is
Failedwith messagehook job <name> failed. - Retried syncs fail identically because the underlying Job command still errors.
Common Root Causes
- The hook command genuinely fails — a database migration, schema check, or smoke test inside the hook exits non-zero on every attempt.
- Missing dependency at hook time — the hook runs before the resource it needs exists (e.g. a migration Job runs before the DB Service/secret is created).
- Bad image or config — wrong image tag, missing env var, or an unmounted secret makes the pod fail on startup.
backoffLimittoo low — a transient failure (DB not yet reachable) exhausts abackoffLimit: 0before the dependency is ready.- OOM or resource limits — the hook pod is
OOMKilledand every retry dies the same way. - Leftover hook resource — a previous failed Job with the same name blocks recreation because the hook
delete-policydid not clean it up.
Diagnostic Workflow
Identify the Application and the failing hook resource:
argocd app get <app> --show-operation
kubectl -n <ns> get jobs -l argocd.argoproj.io/instance=<app>
Inspect the failed Job and its conditions:
kubectl -n <ns> get job <hook-job> -o jsonpath='{.status.conditions}' | jq
kubectl -n <ns> describe job <hook-job>
Read the actual failure from the pod logs — this is where the real error lives:
kubectl -n <ns> get pods --selector job-name=<hook-job>
kubectl -n <ns> logs job/<hook-job> --all-containers --tail=200
kubectl -n <ns> describe pod <hook-pod> | grep -iA3 'State\|Reason\|Exit'
Check the hook annotations to understand phase and cleanup policy:
kubectl -n <ns> get job <hook-job> -o jsonpath='{.metadata.annotations}' | jq
# argocd.argoproj.io/hook: PreSync
# argocd.argoproj.io/hook-delete-policy: HookSucceeded
Review the Argo CD controller view of the operation:
argocd app get <app> -o json | jq '.status.operationState.message'
Example Root Cause Analysis
A team’s PreSync hook db-migrate-presync failed every sync with BackoffLimitExceeded. kubectl get job showed 0 successful completions and backoffLimit: 2. Pod logs revealed the truth:
error: could not connect to database: dial tcp 10.0.14.3:5432: connect: connection refused
The migration hook was scheduled in the same sync wave as the Postgres StatefulSet, so it ran before the database was accepting connections. With backoffLimit: 2, all three attempts fired within ~40 seconds — long before Postgres was ready — and the Job gave up.
The fix had two parts: move the migration hook to a later sync wave with argocd.argoproj.io/sync-wave: "1" so the database provisions first, and raise backoffLimit and add an init wait so a slow-starting DB does not exhaust retries. After correcting the ordering, the hook connected on its first attempt and the sync completed. The leftover failed Job was cleared with kubectl delete job db-migrate-presync before the retry so the new hook could be created.
Prevention Best Practices
- Order hooks with
argocd.argoproj.io/sync-waveso migrations and checks run after their dependencies are provisioned. - Make hook commands wait for their dependencies (an init container or a
wait-forloop) rather than relying on retry timing. - Set a realistic
backoffLimitandactiveDeadlineSecondsso transient slowness does not exhaust retries but a truly broken hook still fails fast. - Set
hook-delete-policy: HookSucceeded(orBeforeHookCreation) so leftover failed Jobs do not block recreation. - Give hook pods resource requests/limits sized to avoid
OOMKilled, and log enough to diagnose fromkubectl logsalone. - Test hooks in a staging Application before they gate production syncs.
Quick Command Reference
# See the failing operation and hooks
argocd app get <app> --show-operation
kubectl -n <ns> get jobs -l argocd.argoproj.io/instance=<app>
# Root-cause the Job
kubectl -n <ns> describe job <hook-job>
kubectl -n <ns> logs job/<hook-job> --all-containers --tail=200
# Inspect hook annotations (phase, delete policy, wave)
kubectl -n <ns> get job <hook-job> -o jsonpath='{.metadata.annotations}' | jq
# Clear a leftover failed hook Job, then retry
kubectl -n <ns> delete job <hook-job>
argocd app sync <app>
Conclusion
BackoffLimitExceeded on an Argo CD hook Job almost never means Argo CD is broken — it means the hook’s own command failed every attempt, and the pod logs will tell you why. The common trap is retry timing: a hook that runs before its dependency is ready burns through a low backoffLimit and stalls the entire sync. Fix the ordering with sync-waves, make hooks wait for what they need, set sane retry limits, and ensure failed Jobs are cleaned up so the next sync starts clean.
Fixed it? Get 500 Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.