Argo CD's control plane, taken apart

A design-level tour of how Argo CD’s control plane is built — the components, the reconcile loop, and no database of its own.

Written against the Argo CD v3.x line · Version-sensitive claims marked current as of 2026-07-01 — re-verify against release notes before you rely on them.

TL;DR

  • One idea, factored into small services. Desired state lives in git, live state lives in your clusters, and the application-controller closes the gap between them. Every other component exists to serve that one loop.
  • There is no database. The only durable state is git plus each cluster's own etcd — where the Application resources and the live objects live. Redis is a throwaway cache you can delete at any time.
  • The loop runs two ways. A git webhook refreshes the one changed app instantly; a periodic timer re-reconciles everything on a schedule as the safety net. Same reconcile, two triggers.
  • It scales by adding replicas, not by rearchitecting. The API server is stateless, the repo-server is horizontally scalable, and the application-controller shards its managed clusters across a StatefulSet.
  • The pieces degrade independently. Lose Redis and you get a slow cache rebuild, not an outage. Lose the repo-server and syncs stall but nothing is destroyed. Any one component's blast radius is small by design.
  • v3.x added a genuinely new component. The commit-server, behind the Source Hydrator, is the first piece with write access to git — split out so that write credential lives in exactly one place.

At a glancethe whole control plane in one diagram

Before we take the components apart one by one, here they all are at once — every service in the control plane, plus the two things it treats as the source of truth: Git (desired state) and your managed clusters (live state). The numbered path traces a single change from the front door to the cluster; the sections that follow walk the same path one box at a time.

The whole control plane, one diagramArgo CD serviceGit = desiredRedis = cacheClusters = liveDex = auththe front doorthe reconcile path: git to clusterWeb UI / CLIhuman + CI/CDAPI serverstateless gatewayDexSSO / OIDCGitdesired staterepo-serverrenders manifestsapplication-controller3-way diffthen syncManaged clusterslive statecommit-serverwrites git (v3.x)notificationsSlack / webhookRedisdisposable cacheApplicationSettemplates AppsApp CRsclonerendercachesynccreates AppswriteemitEvery moving part on one canvas — git and the clusters hold the truth; everything else is a service around the loop.
API serverDeploymentThe front door — Web UI, CLI, and CI/CD reach everything through it (gRPC/REST, auth, RBAC).
DexDeploymentBundled SSO federation that authorises those callers (skip it if you wire an OIDC provider directly).
repo-serverDeploymentClones Git and renders the target manifests (Helm / Kustomize / Jsonnet / plugins).
application-controllerStatefulSetThe engine — pulls the rendered manifests, diffs desired against live, and syncs.
RedisDeploymentDisposable cache for rendered / live / diff results; lose it and it rebuilds.
Managed clustersexternalWhere the diff is applied — the live state lives in each cluster’s etcd, not in Argo CD.
ApplicationSet controllerDeploymentTemplates one Application per target and hands them to the controller.
notifications controllerDeploymentTurns state changes into messages (Slack, webhooks, …).
commit-serverDeploymentNew in v3.x — renders hydrated manifests back into Git (the only component with git write access).

API serverthe stateless gateway every client goes through

The argocd-server is the gRPC/REST gateway — what the Web UI, the argocd CLI, and your CI/CD systems talk to, and the only component most humans ever touch directly.

Its responsibilities are broad but shallow: application management and status reporting, invoking operations (sync, rollback, user-defined actions), managing repository and cluster credentials (as Kubernetes secrets), authentication (delegated to external identity providers), RBAC, and forwarding git webhook events.

Dexoptional SSO federation

The argocd-dex-server is a bundled OIDC provider that federates your identity system into Argo CD — because not every provider speaks OIDC cleanly, and not every OIDC provider exposes what Argo CD needs (like group membership).

The decision:

Repo-serverturns Git into rendered manifests

The argocd-repo-server answers one question: given this repo, revision, and path, what are the Kubernetes manifests? It caches the rendered output in Redis keyed by repo, revision, and path, so repeated reconciliations of an unchanged source cost nothing (--repo-cache-expiration, 24h default).

Inside the repo-serverexternalrepo-serverGit repocloned by rev/pathargocd-repo-serverlocal clone: 1 per instancerender toolsHelmKustomizeJsonnetPluginRendered cachekeyed repo/rev/path · 24happ-controllerpulls via gRPCclonerendercachegRPC manifestsMutating renders serialize: one concurrent generation per instance.1234
1repo-server clones the repo at the requested rev / path
2renders the manifests with the matching tool (Helm / Kustomize / Jsonnet / plugin)
3caches the rendered output in Redis, keyed repo/rev/path (24h)
4serves the manifests to the app-controller over gRPC
The repo-server clones a repo, renders it with the right tool, and caches the output in Redis for the controller to pull over gRPC. Its throughput ceiling is the single serialized clone per instance.

The same app three ways — what you write, what lives in Git, and what the repo-server hands the controller:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/acme/deploy.git
    targetRevision: main
    path: apps/web             # repo-server clones + renders this
  destination:
    server: https://kubernetes.default.svc
    namespace: web
  syncPolicy:
    automated:                 # auto-sync + self-heal
      selfHeal: true
      prune: true
# apps/web/ in the git repo -- a Helm chart (config kept DRY)
apps/web/
  Chart.yaml
  values.yaml                  # replicaCount: 3, image.tag: v1.4.2
  templates/
    deployment.yaml
    service.yaml
# what repo-server returns over gRPC, cached in Redis
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: web
          image: ghcr.io/acme/web:v1.4.2

Application-controllerthe reconciliation engine

The argocd-application-controller is where reconciliation actually happens — a Kubernetes controller that, for every Application, runs the loop the figure below traces.

An Application is not a workload

One term to pin down before we go on: an Application. It sounds like your running app, but it isn’t one. An Application is an Argo CD custom resource — a small record that says “keep this git path synced to this cluster and namespace.” It runs no containers. When Argo syncs it, it applies ordinary Kubernetes objects — a Deployment, a ReplicaSet, some Pods — and those run your code.

The two even live in different places. The Application objects are Kubernetes resources in the argocd namespace, generated from git and reconciled by the application-controller. The DeploymentReplicaSetPods they produce live in the target cluster, run by Kubernetes’ own controllers. Argo just keeps that second set matching git — and reports Synced (matches git?) and Healthy (Pods up?).

An Application is a control-plane pointer, not a workloadThe Application is a CRD stored in etcd — when synced it applies ordinary Kubernetes objects into the target cluster.GitexternalArgo CD control planeargocd nstarget clusterexternalGitOps repodesired configapplication-controllerreconciles each Applicationetcd · the cluster’s backing storeholds the Application CRs — control-plane pointers, not workloadsApplicationweb-prod-uskind: Application (CRD)dest: prod-us · ns webstatus: Synced · HealthyApplicationweb-prod-eukind: Application (CRD)dest: prod-eu · ns webstatus: Synced · Healthya pointer in etcd — the real workload lives in the cluster →Kubernetes APIkube-apiserverDeploymentweb · replicas 3ReplicaSetweb-7c9dPods3 running podsgenerateswatches & reconcilessync → applies manifestscreatescreatescreatesGit · desired configArgo control plane · Application CR in etcdtarget cluster · real workloads
An Application is a control-plane pointer, not a workload. Config lives in the GitOps repo; Argo’s ApplicationSet turns it into Application objects in the argocd namespace; each Application applies its manifests to the target cluster’s Kubernetes API, and Kubernetes’ own controllers turn them into the real Deployment / ReplicaSet / Pods. Argo never runs a container itself.

Two triggers, one loop

The application-controller runs the same reconcile on two triggers. An event — a Git webhook, or an edit to an Application spec — refreshes just the affected app, so a push lands in seconds. A periodic timer (roughly every three minutes by default) re-syncs every app regardless, catching drift the events missed. The figure below contrasts the two.

The application-controller’s two reconcile triggersReconcile is per-Application and independent — a change to one app never re-syncs the others.Webhook — targetedPeriodic — sweep every ~3 minwebuntouchedorderschangedpaymentsuntouchedwebin sync (no-op)ordersdrifted → applypaymentsin sync (no-op)1commit to orders fires a webhook2only orders is marked for refresh3reconciles orders only → deploy1the ~3-min timer fires2re-renders + diffs EVERY apprepo-server SHA cache keeps unchanged apps cheap3unchanged = no-op; drifted app re-appliedWebhook refreshes only the app whose git path changed.Checks all apps, re-applies only the ones that drifted.
Webhook = refresh the one changed app instantly; the periodic sweep re-renders and diffs every app but re-applies only what drifted. Each Application reconciles independently.

A change, end to end

When the webhook fires, the change threads through the whole control plane: the API server records it, the repo-server renders the manifests, and the application-controller diffs desired against live and syncs the delta into the cluster. The numbered path traces one commit from Git to running pods, alongside the ApplicationSet flow that generated the Application in the first place.

Argo CD’s control plane: etcd holds the Application CRs, Redis caches manifestsHow a change flows through Argo CD’s control plane - the A reconcile loop and the B ApplicationSet flow that creates the Applications.GitexternalArgo CD control planeargocd nstarget clusterexternalGitOps reposource of truthAPI servergRPC / RESTrepo-serverrenders manifestsapplication-controllerreconciles + diffsApplicationSet controllergenerates one Application per matchKubernetesAPIcluster: prod-us · ns: webDeploymentReplicaSetPodsRediscache: rendered manifestsetcd(kube-apiserver store)Application CRscustom resourcesApplicationSet CRowns Applicationswebhook (git push)clone /renderasks renderrendered (desired)applywatch (live)createsmark Applicationfor refreshinvalidates repo-server cachecachemanifestsreadwritestatusgit generator readsgit via repo-server(current Argo CD versions)readApplicationSet CRcreatesApplicationsA1A2A3A4A5A6A7A8A9A10A11B1B2B3B4A1–A11 = reconcile loopB1–B4 = ApplicationSet creates the Applications
Reconcile loop (A) — keeps each Application in sync with Git
A1a git push fires a webhook to the API server
A2the API server marks the Application for refresh in etcd, which invalidates the repo-server’s cached manifests
A3the application-controller picks up the refreshed Application from etcd
A4the controller requests a render from the repo-server (repo, path, revision)
A5the repo-server clones / fetches Git and renders the manifests
A6the repo-server returns the manifests to the controller and caches them in Redis
A7the controller reads the cluster’s live state via the watch on the Kubernetes API
A8the controller computes the three-way diff (desired vs live vs last-applied)
A9the controller applies the manifests to the cluster’s Kubernetes API (sync)
A10Kubernetes creates Deployment → ReplicaSet → Pods
A11the controller writes the Application status back to etcd
ApplicationSet flow (B) — creates the Applications the A-loop then reconciles
B1the ApplicationSet controller reads its ApplicationSet CR from etcd (it is a Kubernetes object too)
B2it runs its generators — the git generator reads Git via the repo-server (current Argo CD versions) and the cluster generator reads the cluster list
B3it produces one parameter set per match
B4it renders the template and creates one Application per set into etcd, each owned by the ApplicationSet — those Applications are what the A-loop reconciles
Two flows over Argo CD’s control plane: the reconcile loop A1–A11 keeps each Application in sync, while the ApplicationSet controller’s B1–B4 flow reads its ApplicationSet CR and creates those Applications. The Application CRs live in the cluster’s etcd; Redis is a separate cache for the repo-server’s rendered manifests. Git reaches Argo only through the API server’s webhook, and Argo reads Git through the repo-server.

The periodic sweep

The same machinery also runs on a clock. Instead of one webhook refreshing one app, the timer sweeps every managed Application in turn — re-rendering and re-reconciling each so nothing drifts silently. It is the bulk safety net to the webhook’s targeted, on-demand refresh.

Argo CD’s control plane: etcd holds the Application CRs, Redis caches manifestsThe application-controller’s per-app timer sweeps every Application on its own schedule - the A loop reconciles and re-applies only what drifted; the B ApplicationSet flow creates those Applications.GitexternalArgo CD control planeargocd nstarget clusterexternalGitOps reposource of truthAPI servergRPC / RESTrepo-serverrenders manifestsapplication-controllerreconciles + diffsApplicationSet controllergenerates one Application per matchKubernetesAPIcluster: prod-us · ns: webDeploymentReplicaSetPodsRediscache: rendered manifestsetcd(kube-apiserver store)Application CRscustom resourcesApplicationSet CRowns Applicationsclone /renderasks renderrendered (desired)applywatch (live)createscachemanifestsreadwritestatusgit generator readsgit via repo-server(current Argo CD versions)readApplicationSet CRcreatesApplicationsper-app reconciletimer (~3 min)A1A2A3A4A5A6A7A8A9A10B1B2B3B4A1–A10 = periodic reconcile (timer)B1–B4 = ApplicationSet creates the Applications
Periodic reconcile (A) — the application-controller sweeps every Application on its own timer
A1the application-controller’s per-app reconcile timer fires (~3 min) for this Application
A2it reads the Application from etcd via its informer cache
A3it asks the repo-server to render (repo / path / revision)
A4the repo-server clones / renders — a cache hit in Redis if the commit SHA is unchanged
A5the repo-server returns the manifests and caches them in Redis
A6the controller reads the cluster’s live state via the watch
A7it computes the three-way diff (desired vs live vs last-applied)
A8no-op if in sync — only if it drifted does it apply to the cluster’s Kubernetes API
A9Kubernetes creates Deployment → ReplicaSet → Pods (only if applied)
A10the controller writes the Application status back to etcd
ApplicationSet flow (B) — creates the Applications the A-loop then reconciles
B1the ApplicationSet controller reads its ApplicationSet CR from etcd (it is a Kubernetes object too)
B2it runs its generators — the git generator reads Git via the repo-server (current Argo CD versions) and the cluster generator reads the cluster list
B3it produces one parameter set per match
B4it renders the template and creates one Application per set into etcd, each owned by the ApplicationSet — those Applications are what the A-loop reconciles
The same control plane, driven by the clock instead of a webhook: the application-controller’s per-app reconcile timer (~3 min) sweeps every Application on its own schedule — it renders through the repo-server (a Redis cache hit when the commit SHA is unchanged), computes the three-way diff, and re-applies only what drifted (a no-op when in sync). The ApplicationSet flow B1–B4 still creates the Applications the timer then reconciles; the Application CRs live in etcd and Redis stays a separate manifest cache.

Redisthe disposable cache

Redis holds the caches: rendered manifests, observed live state, and diff results. The docs are blunt — it is "only used as a disposable cache and can be safely rebuilt without service disruption."

If Redis vanishes, nothing permanent breaks: the controller and repo-server rebuild their caches from git and the live clusters. You get a brief load spike on git and the Kube API while caches warm, and slower UI/reconciles for a few minutes. No Application is lost, no sync undone, no desired state forgotten — none of it ever lived in Redis.

ApplicationSet controllerone template, many Applications

A raw Application describes one deployment to one place — it doesn't scale to "this app on all 40 clusters." The ApplicationSet controller (its own controller, a separate project until it merged into core Argo CD in v2.3 — initially beta) owns the ApplicationSet CRD and generates, updates, and deletes Application resources from a single template. v2.3+

The power is in the generators — they produce the parameter sets substituted into the template, one Application per set (Matrix / Merge compose two generators, e.g. every app from Git × every cluster from Cluster). The ApplicationSet controller doesn't reconcile against clusters itself.

ApplicationSet: one template, many ApplicationsApplicationSet controllerApplicationSet controller1 template + generatorsApplication objectsone per parameter setgenerate / update / deletegenerators → parameter setsListhard-coded targetsClusterper registered clusterGitfiles / dirs in a repoPull Requestper open PR (previews)SCM Providerdiscover org reposMatrix / Mergecompose two generatorsCluster Decision / Plugindelegate to CR or HTTPA generator, not a second engine — the application-controller reconciles the output like any hand-written app.123
1a generator produces a set of parameters (List / Cluster / Git / PR / SCM / Matrix)
2the ApplicationSet controller renders the shared template into one Application per set
3the application-controller then syncs each generated Application like any hand-written app
Generators produce the parameter sets; each set becomes one Application. The sync path stays single and consistent no matter how the Applications were created.

Zoom in on where those Applications come from — the same web fleet, seen from the ApplicationSet controller’s side:

How one ApplicationSet becomes many ApplicationsThe ApplicationSet controller’s I/O: a manifest and cluster list in, one Application per matching cluster out.GitexternalArgo CD control planeargocd nstarget clustersexternalApplicationSetmanifesttemplate + generatorcluster listregistered clustersapplication-controllerreconciles each appetcd(holds Application CRs)web-prod-usdest: prod-us · ns webweb-prod-eudest: prod-eu · ns webone parameter set per matching cluster{ name: prod-us }{ name: prod-eu }ApplicationSet controllerruns the generator, renders the templateprod-usns: webprod-euns: webreads manifest+ cluster list123owned by ApplicationSet4hands off5reconciles(A-loop, next)1–5 = ApplicationSet flow: manifest + cluster list → one Application per matching cluster
1reads the ApplicationSet manifest (template + generator) and the cluster list from Git
2runs its generator → one parameter set per matching cluster (prod-us, prod-eu)
3renders the template once per parameter set
4creates one Application object per set (web-prod-us, web-prod-eu), each owned by the ApplicationSet
5hands off → the application-controller then reconciles each Application (the reconcile loop)
The ApplicationSet controller turns one ApplicationSet — a template plus a generator — together with the cluster list into many Application objects, one per matching cluster. It does not sync anything; the application-controller does.

One ApplicationSet fans a single template out into one Application per cluster:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: web-fleet
  namespace: argocd
spec:
  generators:
    - clusters: {}             # one param set per registered cluster
  template:
    metadata:
      name: 'web-{{name}}'     # {{name}} / {{server}} filled per cluster
    spec:
      project: default
      source:
        repoURL: https://github.com/acme/deploy.git
        targetRevision: main
        path: apps/web
      destination:
        server: '{{server}}'
        namespace: web
# the controller renders one Application per cluster the generator saw,
# then syncs each exactly like a hand-written app
web-eu-prod     -> https://eu-prod.example.com
web-us-prod     -> https://us-prod.example.com
web-ap-staging  -> https://ap-staging.example.com
The app-of-apps creation hierarchy: who creates which CRA Root Application (applied once) generates ApplicationSets, which generate the child Applications.GitexternalArgo CD control planeargocd nstarget clusterexternalGitOps reposource of truthapplication-controllersyncs Apps, deploysApplicationSetcontrollerruns generatorsrepo-serverrenders from GitRediscache: rendered manifestsetcd(kube-apiserver store)Root Applicationthe app-of-apps parentApplicationSet CRsgenerated by the Root app syncchild Application CRsone per generator matchcluster: prod-us · ns: webDeploymentReplicaSetPodssourcerenderscachesdeploys workloadscreatesapply the Root Application oncekubectl / CI / Terraform — the only manual stepcreatessyncscreatesreads12341–4 = creation order — the application-controller drives 2 & 4, the ApplicationSet controller drives 3ApplicationSets are optional; this is the app-of-apps path — a parent Application generates ApplicationSets, which generate the child Applications.
1you apply the Root Application once to etcd (kubectl / CI / Terraform) — the only manual step; GitOps cannot create its own entry point
2the application-controller syncs the Root Application (rendered from Git via the repo-server) and applies its manifests — which are ApplicationSet CRs — into etcd
3the ApplicationSet controller reads each ApplicationSet CR, runs its generators, and creates one child Application per match into etcd
4the application-controller reconciles each child Application and deploys its workloads (Deployment → ReplicaSet → Pods) to the target cluster
The app-of-apps hierarchy: a Root Application (applied once) → the application-controller creates the ApplicationSet CRs → the ApplicationSet controller creates the child Applications → the application-controller deploys their workloads. Each level is created by a controller reconciling the level above.

Notifications controllerturning state changes into messages

The notifications controller (originally the separate argocd-notifications project, now folded into core) watches Application state and emits notifications. Its model is two-part:

The rendered message goes out through a configured service — Slack, a generic webhook, email. Like Dex, it sits off the critical path: if it's down, deployments still reconcile; you just aren't told about them.

Commit-serverwrites hydrated manifests back to Git (v3.x)

The newest and most architecturally interesting addition. Be precise about its status: the Source Hydrator is an alpha feature, introduced in the v3.x line and actively evolving through v3.1 and v3.3. v3.x, alpha, re-verify

The problem it solves. Helm and Kustomize keep config DRY but hide the actual manifests that hit the cluster — when something breaks at 2am you have to reproduce the render locally to see it. The Source Hydrator implements the "rendered manifest pattern": it renders the templates and commits the plain manifests back to a git branch before syncing, so git shows exactly what will be applied.

The flow uses three fields on spec.sourceHydrator:

Output must be deterministic — the same dry commit must always hydrate to the same manifests, so no unpinned dependencies or non-deterministic template functions. (And don't pair it with secret-injecting tools like Helm+SOPS, or you'd commit secrets to git; use a secrets operator on the destination instead.)

Why the commit-server is its own component

Pushing to git needs write credentials, and every other component that touches git — the repo-server — only ever reads. Rather than hand write access to the read component, v3.x introduced a dedicated argocd-commit-server whose entire job is git writes for hydrated manifests.

It exposes a single gRPC call, CommitManifests, carrying the target repo and branch, the dry commit's provenance (full SHA256, author, message, time), and the rendered manifests. The controller builds the request; the commit-server performs the push.

The rationale is blast radius. Git write credentials now live in one small, single-purpose service instead of being smeared across the component that also clones arbitrary repos and runs templating tools. If something gets push access to your source of truth, you want it to be the least-exposed component you have — the whole reason the commit-server is a separate box.

Source Hydrator: the rendered-manifest patternThe repo-server renders; the commit-server commits hydrated YAML back to git; the controller syncs that branch.GitexternalArgo CD control planeargocd nstarget clusterexternaldrySourceDRY templatesrepoURL + pathsyncSourcehydrated YAMLsync branchrepo-serverrenders manifestscommit-serveronly git-write credsCommitManifests gRPCapplication-controllersyncs hydrated branchKubernetesAPIcluster: prod-us · ns: webDeploymentReplicaSetPodsclone / readplain manifestsgit writesyncs hydrated branchapplycreates12345red = git-write path (commit-server only)1–5 = Source Hydrator flow
1repo-server reads the DRY templates from drySource
2renders them and hands the plain manifests to the commit-server
3commit-server commits the hydrated YAML back to git — CommitManifests gRPC, the only git-write creds
4app-controller syncs the hydrated syncSource branch
5and applies the plain manifests to the cluster
The rendered manifest pattern: templates are rendered and the plain YAML is committed back to git before syncing, so git shows exactly what hits the cluster. The commit-server is the only component with write access — git-write creds live in one small box.

The Source Hydrator in YAML — the spec, the DRY template on main, and the plain manifest the commit-server writes to the sync branch:

spec:
  sourceHydrator:
    drySource:                 # templates live here (DRY)
      repoURL: https://github.com/acme/deploy.git
      path: apps/web
      targetRevision: main
    syncSource:                # plain YAML is committed + synced here
      targetBranch: env/prod
      path: apps/web
    hydrateTo:                 # optional staging branch for review
      targetBranch: env/prod-next
# apps/web/values.yaml on main -- the actual manifests are hidden
# behind Helm templating until they are rendered
replicaCount: 3
image:
  repository: ghcr.io/acme/web
  tag: v1.4.2
# apps/web/manifests.yaml on env/prod -- plain YAML the
# commit-server wrote; git now shows exactly what hits the cluster
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  template:
    spec:
      containers:
        - {name: web, image: ghcr.io/acme/web:v1.4.2}

The state modelthree stores of truth, one loop

Argo CD is a GitOps controller, and it keeps its truth in exactly three stores. Git holds the desired state — the manifests, or the templates that render into them. The managed cluster’s own etcd holds the live state: the running Kubernetes objects and the Application custom resources both live there. And Redis is a disposable cache of rendered manifests — throwaway, safe to delete, never a source of truth. Git and the cluster’s etcd are external to Argo CD; only Redis is its own.

One reconcile loop runs across all three. The application-controller (1) reads the desired manifests from Git — rendered by repo-server, (2) reads the live state back from the cluster’s etcd, (3) diffs desired against live, (4) syncs the delta into the cluster, and (5) caches the rendered manifests in Redis so the next pass stays cheap. That’s the whole storage story: no relational database, no external queue, no object store. Wipe every Argo CD pod and its Redis, reinstall, and it rebuilds its entire world from Git and the clusters — which is what makes everything else as disposable as it is. Every component below either feeds that loop, runs it, or reacts to its output.

Three stores of truth, one loopexternal storeRedis · internal cachereconcile loopexternalArgo CD control planetarget cluster · externalGitSTOREdesired statemanifests / templatescluster etcdSTORElive state+ Application CRsrunning K8s objectsRedisSTOREdisposable cache · throwawayrepo-serverrenders desiredapplication-controllerruns the reconcile loopdiffs desired vs live12453No durable DB · no queue · no object store — wipe every pod and it rebuilds from Git + the clusters.
1application-controller reads the desired manifests from Git — rendered by repo-server
2reads the live state back from the managed cluster’s etcd
3diffs desired against live inside application-controller
4syncs the delta into the cluster, applying desired over live
5caches the rendered manifests in Redis to keep the next pass cheap
The entire storage story: Git holds desired, the cluster’s etcd holds live plus the Application CRs, Redis is a throwaway cache — and one controller loop closes the gap. Nothing durable lives inside Argo CD.

Putting it togetherone change through the whole loop

Argo CD’s control plane: etcd holds the Application CRs, Redis only caches manifestsTrace one git push end-to-end - the reconcile path that ends in Deployment → ReplicaSet → Pods.GitexternalArgo CD control planeargocd nstarget clusterexternalGitOps reposource of truthAPI servergRPC / RESTrepo-serverrenders manifestsapplication-controllerreconciles + diffsRediscache: rendered manifestsetcd(kube-apiserver store)Application CRscustom resourcesKubernetesAPIcluster: prod-us · ns: webDeploymentReplicaSetPodsnotifications-controllertrigger → sends a messagewebhook (git push)clone /rendermark Applicationfor refreshinvalidates repo-server cacheasks renderrendered (desired)cachemanifestsreadwritestatusapplywatch (live)createswatchesapp status12345671–7 = one commit’s path through the control plane
1a developer pushes a commit — a webhook (or 3-min poll) notifies the API server
2the API server marks the Application for refresh in etcd, which invalidates the repo-server’s cached manifests
3the application-controller requests a render and the repo-server clones Git and renders the desired manifests over gRPC
4the controller diffs desired against the live watch cache (three-way) via the Kubernetes API
5it syncs — applying the manifests to the cluster, which creates Deployment → ReplicaSet → Pods
6Redis caches the rendered manifests throughout — a cache, never a source of truth
7notifications fire — a trigger sends a message
Trace one commit and the components snap into place. The Application CRs live in the cluster’s etcd; Redis is only a cache for the repo-server’s rendered manifests. Nowhere in the path is there a write to an Argo CD datastore, because there isn’t one.

Nowhere in that path is there a write to an Argo CD datastore, because there is no such datastore. The desired state was already in git; the live state ends up in the cluster's etcd; Redis just made it faster.

In productionthe HA topology worth running

Argo CD ships HA manifests for production that encode the scaling story component by component:

ComponentShapeHA move
API serverStateless Deployment3+ replicas; set ARGOCD_API_SERVER_REPLICAS to match
Repo-serverDeploymentScale replicas for render throughput
Application-controllerStatefulSetAdd replicas as the managed fleet grows
RedisCacheHA mode: Redis + Sentinel (3 instances) behind HAProxy
DexIn-memorySingle replica (multi-replica is inconsistent); off the sync path
ApplicationSet / NotificationsControllersRun them; both are off the critical sync path
Commit-server v3.xgRPC serviceOnly if you use the Source Hydrator; holds git write creds

Two practical constraints in the HA install: it wants at least three nodes (pod anti-affinity rules spread the replicas), and IPv6-only clusters aren't supported.

Watch-outsthe pitfalls to plan for


GroundingSources

Version-sensitive claims (Source Hydrator/commit-server maturity, v3.x feature states) are current as of 2026-07-01 and should be re-verified against the Argo CD release notes before you rely on them.