Sync, health & status
Synced, healthy, and diffs.
Argo CD watches two things at once, and it keeps the answers on separate pages on purpose. A building inspector carries two clipboards. The first asks one question: does the building match the blueprint? The second asks something completely different: do the lights come on, does water run, does the elevator move? A building can match its blueprint down to the last screw and still sit dark and cold. It can also be warm and lit while someone has quietly added a room that was never on the plans. Those clipboards are Argo CD's two statuses, and every Application (the object that ties one Git source to one cluster destination) carries one of each. Sync status compares the live cluster against Git. Health status checks whether the workloads actually run. You read both, every time, because neither one answers the other's question.
Two Clipboards: Sync And Health
Sync status has a short vocabulary. Synced means the live objects in the cluster are identical to what Git declares for the app's target revision (the branch, tag, or commit the Application points at). OutOfSync means they differ, and Argo CD can show you the exact difference. Unknown means the comparison itself failed, usually because the repository was unreachable or a rendering step (Helm or Kustomize turning templates into plain manifests) broke. Health status has a longer vocabulary, because working has more shades than matching: Healthy, Progressing (a rollout is still in flight), Degraded (something failed), Missing (Git declares an object the cluster does not have), and Suspended (paused on purpose, like a CronJob, a task Kubernetes runs on a schedule, that you switched off). The two vocabularies never mix. An app holds one value from each list, and the pair is the whole story.
Git is the desired state, the blueprint. The live state is what the Kubernetes API server actually holds in its records, the real building. Argo CD renders the Git manifests, compares them field by field against the live objects, and reports the result per app and per object. Here is what one app looks like.
$ argocd app get payments-api
Name: argocd/payments-apiProject: defaultServer: https://kubernetes.default.svcNamespace: paymentsURL: https://argocd.acme.internal/applications/payments-apiSource:- Repo: https://git.acme.internal/apps/payments.gitTarget: v1.4.0Path: k8s/overlays/prodSyncWindow: Sync AllowedSync Policy: <none>Sync Status: OutOfSync from v1.4.0 (9f3c1ad)Health Status: HealthyGROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGEService payments payments-api Synced Healthyapps Deployment payments payments-api OutOfSync HealthyConfigMap payments payments-api Synced
Read the header first. This app targets v1.4.0 but is OutOfSync, so the live cluster is running something else (the previous image). Health Status is Healthy, because the pods that are running, old as they are, are up and serving traffic. The table underneath breaks it down per object. The Service and ConfigMap already match Git; only the Deployment is OutOfSync. Health is blank for the ConfigMap because a ConfigMap has nothing to be healthy or unhealthy about, it only holds data. One screen, and you know the cluster is stable but a version behind.
Reading The Diff
The diff is the redline drawing, the sheet where the inspector marks every place the building departs from the blueprint. For a defender it doubles as a tamper alarm. Anything that differs between Git and the cluster shows up here, whether you put it there or somebody else did.
$ argocd app diff payments-api
===== apps/Deployment payments/payments-api ======90c90< image: registry.acme.internal/payments-api:v1.3.2---> image: registry.acme.internal/payments-api:v1.4.0
The left side (the < lines) is the live cluster; the right side (the > lines) is what Git wants. This particular diff is your own pending upgrade: Git moved the image to v1.4.0 and the cluster still runs v1.3.2. The command exits 0 when there is no difference and 1 when there is, which makes it a clean gate in a script or a CI job (continuous integration, the automation that runs on every commit): run the diff, and a non-zero exit code means the cluster and Git have drifted apart.
Now the security case. The same diff catches changes you did not make. Suppose an attacker with cluster access strips the runAsNonRoot guard off the Deployment so a container can run as root (the all-powerful account inside a Linux system). They never touch Git, so a code reviewer would never see it. Argo CD sees it on its next comparison.
$ kubectl -n payments patch deploy payments-api --type=json \-p='[{"op":"remove","path":"/spec/template/spec/containers/0/securityContext/runAsNonRoot"}]'$ argocd app diff payments-api
deployment.apps/payments-api patched===== apps/Deployment payments/payments-api ======118a119> runAsNonRoot: true
Argo CD wants runAsNonRoot: true, because it is in Git. The live object no longer has it, so the diff shows an add (the a in 118a119) putting the field back. The app flips to OutOfSync and names the exact field that was removed. Drift like this is a detection surface. A monitor that alerts whenever a production Application goes OutOfSync turns someone edited the cluster by hand into a page you can answer. With an automated self-heal policy (a later lesson) Argo CD would revert the tamper on its next pass; even without it, the diff is a receipt of exactly what changed.
There is one blind spot to know about before you trust the diff completely. A setting called ignoreDifferences tells Argo CD to skip named fields when it compares, which is handy for values another controller legitimately owns (a replica count driven by an autoscaler, a sidecar injected by a service mesh). The cost is that anything you ignore can be changed on the live cluster and never show as OutOfSync. Keep those rules as narrow as you can, and never blanket-ignore an image, a securityContext (the block that says how a container is allowed to run), or an RBAC (role-based access control, who is allowed to do what) field to quiet a noisy diff. Those are exactly the places you want tampering to be loud.
Health Is Computed, Not Reported
Health is not a field the app fills in about itself. Argo CD works it out, the way a careful mechanic ignores the dashboard warning light and checks the engine directly. For every object it applies a rule keyed to that object's kind. A Deployment reads Healthy when its observedGeneration has caught up to the spec you asked for, the counts of updated and available replicas meet that spec, and there is no ProgressDeadlineExceeded condition. While the new pods are still coming up it reads Progressing. If the rollout blows past its progress deadline, it reads Degraded. An object Git declares but the cluster lacks reads Missing. A CronJob you switched off, its spec.suspend field set to true, reads Suspended.
The built-in rules cover the standard kinds. They do not know about your custom resources. When you install a CustomResourceDefinition (a CRD, the mechanism that teaches Kubernetes a brand new object type, such as a managed database) Argo CD has no idea what healthy means for it, so by default it calls the object Healthy the moment the object exists. That is dangerous for a promotion gate: a database that was created but never actually came up would read Healthy and wave a broken release straight through. You fix it by teaching Argo CD a rule in Lua (a small scripting language), added to the argocd-cm ConfigMap that configures the server.
apiVersion: v1kind: ConfigMapmetadata:name: argocd-cmnamespace: argocddata:# health.<group>_<kind>: the Lua returns hs.status + hs.messageresource.customizations.health.platform.acme.io_Database: |hs = {}hs.status = "Progressing"hs.message = "Waiting for the database to report Ready"if obj.status ~= nil and obj.status.conditions ~= nil thenfor _, c in ipairs(obj.status.conditions) doif c.type == "Ready" and c.status == "True" thenhs.status = "Healthy"hs.message = c.messageendif c.type == "Ready" and c.status == "False" thenhs.status = "Degraded"hs.message = c.messageendendendreturn hs
Now the object reads Progressing until its Ready condition turns true, Healthy once it does, and Degraded if that condition ever turns false. Health becomes an honest signal for that kind instead of a rubber stamp. Argo CD ships built-in checks for many popular CRDs already; you write these for the ones it does not know, and for anything a promotion gate is going to trust.
Syncing And Waiting For Healthy
Syncing is handing the builder the corrected blueprint and telling them to make the building match it. For an OutOfSync app, argocd app sync applies the Git state to the cluster. You can preview it first with --dry-run, delete objects Git no longer declares with --prune, and target a single object with --resource. The moment the apply lands, sync status flips to Synced. Health does not flip with it, because the pods still have to roll.
$ argocd app sync payments-api
TIMESTAMP GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE2026-07-20T10:15:32+00:00 Service payments payments-api Synced Healthy2026-07-20T10:15:32+00:00 apps Deployment payments payments-api OutOfSync ProgressingName: argocd/payments-apiProject: defaultServer: https://kubernetes.default.svcNamespace: paymentsSyncWindow: Sync AllowedSync Policy: <none>Sync Status: Synced to v1.4.0 (9f3c1ad)Health Status: ProgressingOperation: SyncSync Revision: 9f3c1adf5c2b7e1a0d4c9b8e6f3a2d1c0b9a8f7ePhase: SucceededStart: 2026-07-20 10:15:32 +0000 UTCFinished: 2026-07-20 10:15:41 +0000 UTCDuration: 9sMessage: successfully synced (all tasks run)GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGEService payments payments-api Synced Healthyapps Deployment payments payments-api Synced Progressing Waiting for rollout to finish: 1 of 3 updated replicas are available...
Notice the split in the result. Phase is Succeeded and Sync Status is Synced, because the manifests applied cleanly. Health Status is Progressing, because the new pods are still coming up. If you stopped reading here you would call the deploy done. It is not done. This is the exact gap where the word Synced fools people, so do not stop here.
$ argocd app wait payments-api --health --timeout 300
2026-07-20T10:15:43+00:00 apps Deployment payments payments-api Synced Progressing Waiting for rollout to finish: 1 of 3 updated replicas are available...2026-07-20T10:16:04+00:00 apps Deployment payments payments-api Synced HealthyName: argocd/payments-apiSync Status: Synced to v1.4.0 (9f3c1ad)Health Status: Healthy
argocd app wait blocks until the conditions you name are met. --health waits for Healthy, --sync waits for Synced, --operation waits for a running sync to finish, and --timeout caps the wait so a stuck rollout fails your job instead of hanging it forever. This is the line you drop into a promotion pipeline: sync, then wait for Healthy, and let a non-zero exit on timeout or Degraded stop the promotion cold. That single command turns I hope it came up into a gate that blocks a bad release from moving to the next environment.
Try this
Run argocd app get payments-api on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: synced is not the same as working. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.