Cheveo Blog
gitops-argocd 12 min read ·

ArgoCD OutOfSync Debugging: 12 Causes, 1 Workflow

OutOfSync is the most common ArgoCD problem. 12 causes with argocd commands, real outputs, and a decision tree that finds every cause in under 5 minutes.

Clemens Christen
Clemens Christen Certified Kubernetes Administrator (CKA)

TL;DR — OutOfSync means Git state ≠ cluster state. It’s not an error, it’s the normal result of any deviation. 12 causes cover 95% of all cases — from harmless server-side defaults to real drift problems. The 4-command workflow (diff, get, logs, events) finds the cause in under 5 minutes.

🔖 Just want the YAMLs? Here’s the interactive ArgoCD OutOfSync cheatsheet — with a copy button per pattern and common mistakes. Bookmark recommended.

What OutOfSync actually means

ArgoCD compares two states at regular intervals (default: every 3 minutes):

  1. Desired State — what’s in your Git repository
  2. Live State — what’s actually running in the cluster

Any deviation between the two results in the OutOfSync status. This can mean:

  • A field in a manifest was manually changed in the cluster (real drift)
  • Kubernetes added a default field that’s not in your manifest (phantom diff)
  • A webhook injected a label or annotation (mutation)
  • The sync failed and the cluster has the old state

The difficulty: ArgoCD doesn’t distinguish between “dangerous drift” and “harmless default.” Both are OutOfSync. You have to.

The 4-command workflow

Regardless of cause — these four commands always run first.

Step 1: Show the diff

argocd app diff <app-name>

Shows exactly which fields differ between Git and cluster. This is the most important command. If you only know one: this one.

Typical output for server-side defaults:

===== apps/Deployment default/api-server =====
--- desired
+++ live
@@ -18,6 +18,8 @@
   spec:
     containers:
     - name: api
+      terminationMessagePath: /dev/termination-log
+      terminationMessagePolicy: File
       image: registry.example.com/api:v1.2.3
+    dnsPolicy: ClusterFirst
+    schedulerName: default-scheduler

See fields like terminationMessagePath, dnsPolicy, schedulerName? Those are Kubernetes defaults that the API server sets automatically. Your manifest is correct — Kubernetes just fills in the blanks.

Step 2: Check app status

argocd app get <app-name>

Shows the overall status: Sync Status, Health Status, last sync operation, and which resources are affected.

Name:               default/api-server
Sync Status:        OutOfSync
Health Status:      Healthy
Last Sync:          2026-05-25 10:15:03 +0200 CEST (ComparedTo)
Sync Resources:     3 out of sync

Key insight: Healthy + OutOfSync usually means harmless diffs. Degraded + OutOfSync is a real problem.

Step 3: Check controller logs

kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --tail=50

Shows sync errors, RBAC issues, and timeouts that aren’t visible in the UI.

Step 4: Check events

kubectl get events -n <target-namespace> --sort-by=.lastTimestamp | tail -20

Shows Kubernetes events triggered by the sync: failed deployments, admission webhook rejections, quota limits.

The 12 most common causes

1. Server-side defaults

Symptom: OutOfSync on fields you never set (terminationMessagePath, dnsPolicy, schedulerName, revisionHistoryLimit).

Cause: The Kubernetes API server fills in default values when creating a resource. Your manifest doesn’t have these fields, the live state does. ArgoCD sees a difference.

Fix:

# In the Application spec:
spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/template/spec/dnsPolicy
        - /spec/template/spec/schedulerName
        - /spec/revisionHistoryLimit

Or globally for the entire cluster (ArgoCD 2.5+):

# argocd-cm ConfigMap:
resource.customizations.ignoreDifferences.apps_Deployment: |
  jsonPointers:
    - /spec/template/spec/dnsPolicy
    - /spec/template/spec/schedulerName

2. Mutating webhooks

Symptom: OutOfSync on labels, annotations, or entire containers you didn’t define (e.g., sidecar.istio.io/inject, Linkerd proxy container).

Cause: A mutating admission webhook (Istio, Linkerd, Vault Agent, OPA Gatekeeper) modifies the resource after apply. Git doesn’t know about the injected sidecar → diff.

Fix: ignoreDifferences for the specific paths:

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jqPathExpressions:
        - .spec.template.metadata.annotations."sidecar.istio.io/status"
        - .spec.template.spec.initContainers[] | select(.name == "istio-init")
        - .spec.template.spec.containers[] | select(.name == "istio-proxy")

3. Helm template vs. Helm install

Symptom: OutOfSync on Helm charts, even though helm install works locally.

Cause: ArgoCD uses helm template (client-side), not helm install (server-side). helm template has no cluster access — .Capabilities.APIVersions, lookup functions, and server-side hooks don’t work or return different results.

Diagnosis:

# Compare locally what ArgoCD sees:
helm template <release> <chart> --values values.yaml > /tmp/argocd-view.yaml
# vs. what helm install produces:
helm install <release> <chart> --values values.yaml --dry-run > /tmp/helm-view.yaml
diff /tmp/argocd-view.yaml /tmp/helm-view.yaml

Fix: Avoid lookup and .Capabilities in templates, or set ApiVersions in the ArgoCD Application spec:

spec:
  source:
    helm:
      apiVersions:
        - monitoring.coreos.com/v1

4. Immutable fields

Symptom: Sync fails with field is immutable — not just OutOfSync, but SyncFailed.

Cause: Kubernetes doesn’t allow changes to certain fields after creation: spec.selector.matchLabels on Deployments, spec.clusterIP on Services, spec.volumeName on PVCs.

Fix: The resource must be deleted and recreated. In ArgoCD:

argocd app sync <app-name> --resource <group>/<kind>/<name> --replace

Or safer: sync with replace annotation:

metadata:
  annotations:
    argocd.argoproj.io/sync-options: Replace=true

5. RBAC / permission errors

Symptom: OutOfSync, sync attempt fails with forbidden in the controller log.

Cause: The ArgoCD Application Controller doesn’t have the necessary cluster permissions to create or modify the resource.

Diagnosis:

kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --tail=100 | grep -i "forbidden\|unauthorized"

Fix: Extend the ArgoCD service account’s ClusterRole/RoleBinding, or whitelist the missing API groups in the ArgoCD Project.

6. Missing namespace

Symptom: OutOfSync with namespace not found in the sync log.

Cause: The manifest references a namespace that doesn’t exist yet. ArgoCD doesn’t create namespaces automatically (unless the namespace is part of the manifest).

Fix: Include the namespace as its own resource in the same Application manifest and use sync waves to ensure it’s created first:

apiVersion: v1
kind: Namespace
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "-1"
2-Day Hands-on Workshop

GitOps with ArgoCD - from push to production deploy

From the App-of-Apps pattern to progressive delivery: everything you need for GitOps in production.

View workshop details

7. Resource quota exceeded

Symptom: OutOfSync + exceeded quota in events.

Cause: The namespace has a ResourceQuota and the sync operation would exceed the limit.

Diagnosis:

kubectl describe resourcequota -n <namespace>
kubectl get events -n <namespace> | grep -i quota

Fix: Either increase the quota or adjust the resource requests/limits in the manifest.

8. Kustomize build errors

Symptom: OutOfSync + kustomize build error in the controller log.

Cause: The Kustomize overlay has an error — missing base, invalid patch, incompatible API versions.

Diagnosis:

# Test locally:
kustomize build overlays/production/

Fix: Fix the Kustomize error. Most common cause: relative paths in kustomization.yaml that don’t resolve in the ArgoCD context (which clones the repo to /tmp).

9. Finalizer blocking deletion

Symptom: OutOfSync on a resource that should be deleted. Resource stays with status Terminating.

Cause: A finalizer on the resource prevents deletion because the associated controller is no longer running or unreachable.

Diagnosis:

kubectl get <resource> <name> -o jsonpath='{.metadata.finalizers}'

Fix:

kubectl patch <resource> <name> --type merge -p '{"metadata":{"finalizers":null}}'

10. Sync loop (auto-sync + phantom diff)

Symptom: ArgoCD syncs every 3 minutes, status alternates between Synced and OutOfSync, application controller logs show constant sync operations.

Cause: Auto-sync is enabled. Every sync immediately produces OutOfSync again due to server-side defaults or webhooks. ArgoCD syncs again. Loop.

Diagnosis:

argocd app get <app-name> --show-operation
# Shows recent sync operations with timestamps

Fix: ignoreDifferences for the causing fields (cause 1 or 2), or enable server-side diff:

# argocd-cm ConfigMap:
application.resourceTrackingMethod: annotation+label
controller.diff.server.side: "true"

11. Git repository unreachable

Symptom: OutOfSync + ComparisonError + failed to load initial state of resource in the app status.

Cause: ArgoCD can no longer reach the Git repository — expired credentials, rotated SSH key, network issue.

Diagnosis:

argocd repo list
argocd repo get <repo-url>
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-repo-server --tail=50

Fix: Update repository credentials:

argocd repo add <url> --username <user> --password <token>

12. Stale cache

Symptom: OutOfSync disappears after hard refresh but returns after the next sync cycle. Or: changes in Git are not detected.

Cause: The ArgoCD repo server caches cloned repositories. With large repos or many branches, the cache can become stale.

Fix:

# Soft refresh (re-compare without cache invalidation):
argocd app get <app-name> --refresh

# Hard refresh (fully invalidate cache):
argocd app get <app-name> --hard-refresh

If the problem persists, restart the repo server:

kubectl rollout restart deployment argocd-repo-server -n argocd

Server-side diff: the nuclear option

Starting with ArgoCD 2.5+, you can enable server-side diff. Instead of running helm template locally, ArgoCD sends the manifest to the API server with kubectl diff --server-side. The server applies defaults and webhooks before comparison. Result: 80% fewer phantom diffs.

# argocd-cm ConfigMap:
controller.diff.server.side: "true"

Caution: Server-side diff requires that the ArgoCD service account has PATCH permissions on all managed resources. Check your RBAC before enabling this.

Decision tree: quick diagnosis

OutOfSync
├── argocd app diff shows only default fields?
│   └── Yes → Cause 1 (server-side defaults) or 2 (webhooks)
│        → configure ignoreDifferences
├── Sync attempt fails?
│   ├── "field is immutable" → Cause 4 → Replace sync
│   ├── "forbidden" → Cause 5 → Check RBAC
│   ├── "namespace not found" → Cause 6 → Sync waves
│   ├── "exceeded quota" → Cause 7 → Adjust quota
│   └── Kustomize/Helm error → Cause 3 or 8 → Test locally
├── Resource stuck in Terminating?
│   └── Cause 9 → Remove finalizer
├── Sync loop (constant re-sync)?
│   └── Cause 10 → ignoreDifferences or server-side diff
├── ComparisonError?
│   └── Cause 11 → Check repo credentials
└── Hard refresh fixes it temporarily?
    └── Cause 12 → Restart repo server

Where to go from here

In the GitOps with ArgoCD Workshop we build a complete ArgoCD setup including App-of-Apps pattern, Sealed Secrets, and drift detection — and intentionally break syncs so you can apply the patterns from this article under guidance.

Related from our series:

2-Day Hands-on Workshop

GitOps with ArgoCD - from push to production deploy

From the App-of-Apps pattern to progressive delivery: everything you need for GitOps in production.

View workshop details
Free · 30 minutes

Need a second opinion on your cluster?

Book a free 30-minute Kubernetes health check. We review your setup and give concrete recommendations, no sales pitch.

Book a slot