CoursesArgo CDSync, health & status

Sync, health & status

Synced, healthy, and diffs.

Advanced12 min · lesson 3 of 12

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.

Two independent axes
Sync status: does live match Git?
Synced
identical to the target revision
OutOfSync
differs; a diff is available
Unknown
comparison failed (repo/render error)
Health status: do the workloads run?
Healthy
meets its kind's rule
Progressing
rollout still in flight
Degraded
failed or deadline exceeded
Missing
in Git, absent in the cluster
Suspended
paused on purpose
Every app carries one value from each column. Synced + Degraded (shipped what Git said, and it is broken) and OutOfSync + Healthy (running fine, but drifted from Git) are both normal states, and both tell you something.

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.

terminal
$ argocd app get payments-api
output
Name: argocd/payments-api
Project: default
Server: https://kubernetes.default.svc
Namespace: payments
URL: https://argocd.acme.internal/applications/payments-api
Source:
- Repo: https://git.acme.internal/apps/payments.git
Target: v1.4.0
Path: k8s/overlays/prod
SyncWindow: Sync Allowed
Sync Policy: <none>
Sync Status: OutOfSync from v1.4.0 (9f3c1ad)
Health Status: Healthy
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service payments payments-api Synced Healthy
apps Deployment payments payments-api OutOfSync Healthy
ConfigMap 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.

terminal
$ argocd app diff payments-api
output
===== 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.

terminal
$ 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
output
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.

argocd-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
# health.<group>_<kind>: the Lua returns hs.status + hs.message
resource.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 then
for _, c in ipairs(obj.status.conditions) do
if c.type == "Ready" and c.status == "True" then
hs.status = "Healthy"
hs.message = c.message
end
if c.type == "Ready" and c.status == "False" then
hs.status = "Degraded"
hs.message = c.message
end
end
end
return 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.

terminal
$ argocd app sync payments-api
output
TIMESTAMP GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
2026-07-20T10:15:32+00:00 Service payments payments-api Synced Healthy
2026-07-20T10:15:32+00:00 apps Deployment payments payments-api OutOfSync Progressing
Name: argocd/payments-api
Project: default
Server: https://kubernetes.default.svc
Namespace: payments
SyncWindow: Sync Allowed
Sync Policy: <none>
Sync Status: Synced to v1.4.0 (9f3c1ad)
Health Status: Progressing
Operation: Sync
Sync Revision: 9f3c1adf5c2b7e1a0d4c9b8e6f3a2d1c0b9a8f7e
Phase: Succeeded
Start: 2026-07-20 10:15:32 +0000 UTC
Finished: 2026-07-20 10:15:41 +0000 UTC
Duration: 9s
Message: successfully synced (all tasks run)
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service payments payments-api Synced Healthy
apps 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.

terminal
$ argocd app wait payments-api --health --timeout 300
output
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 Healthy
Name: argocd/payments-api
Sync 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.

Synced is not the same as working
Synced only says the cluster matches Git. The same app can be Synced and Degraded because the new image crashes on startup or a dependency is down: you shipped exactly what Git declared, and it is broken. The reverse happens too, Healthy and OutOfSync, when the running pods are fine but the cluster has drifted from Git. After every change, read health as well as sync, and gate promotions on wait --health so a Degraded rollout can never be mistaken for a good one.
Quick check
01argocd app get shows Sync Status: Synced and Health Status: Degraded right after a deploy. What does that actually tell you?
Correct — Sync answers 'matches Git?' and health answers 'actually works?'. Synced + Degraded means you shipped exactly what Git said and it failed.
Incorrect — A failed apply shows as a failed sync operation or an OutOfSync app, not Synced; Synced means the manifests landed.
Incorrect — Degraded reflects a real rule failure (like ProgressDeadlineExceeded); it does not self-clear without a fix to the workload.
Incorrect — Drift surfaces as OutOfSync. Synced means live already matches Git, so this is a workload failure, not drift.
02You install a CustomResourceDefinition for a managed Database and sync it. Before you add any custom health rule, how does Argo CD report the Database's health, and why is that a problem for a promotion gate?
Incorrect — Argo CD does not know the custom kind, so it does not default it to Degraded.
Correct — The built-in rules do not cover custom resources, so by default Argo CD calls the object Healthy the moment it exists.
Incorrect — Unknown is a sync-status value, not a health value; the default health for an unknown kind is Healthy.
Incorrect — It does not sit Progressing by default; that only happens once you add a Lua rule that returns Progressing.
03argocd app diff on payments-api prints one hunk, 118a119, with a single line: > runAsNonRoot: true. Nobody changed Git. What does this tell you?
Correct — The > side is what Git wants, and the add (the a in 118a119) means the field is absent live, so it was removed from the cluster.
Incorrect — The > side is what Git already wants; the diff shows the live object no longer has it, not that Git changed.
Incorrect — An add putting the field back means Argo CD wants it present to match Git, not removed.
Incorrect — A securityContext field like runAsNonRoot is exactly the kind of change you want the diff to flag loudly, not ignore.

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.

Related