conndeck blog

ArgoCD vs Flux: After Running Both in Production

For two years, ArgoCD was the answer I gave whenever anyone asked about GitOps. Then last October, an acquisition landed in my lap: a platform team running Flux across forty-something clusters, and a mandate to decide whether we consolidate. So I ran both, side by side, against the same workloads, for about six weeks.

The conclusion annoyed everyone who wanted a winner. Both are excellent. They're also built on genuinely different instincts about what GitOps tooling should be, and the right choice depends on your org's shape more than on any feature matrix. Here's what I actually learned, including the parts the comparison blog posts skip.

Quick answer: Neither is universally better. ArgoCD is a product: a central control plane with a UI, SSO, app-centric RBAC, and native multi-cluster management from one instance. Flux is a toolkit: four small controllers that run inside each cluster, with no UI and no central server, that you compose with plain Kubernetes RBAC. Pick ArgoCD if developers need self-serve visibility and you want one pane of glass. Pick Flux if you're running many clusters, prefer a minimal operational footprint, and your platform team lives in YAML anyway.

Same religion, different churches

ArgoCD thinks in applications. You define an Application CRD -- here's a git repo, here's a path, here's a destination cluster and namespace -- and a central control plane reconciles it, draws you a tree, and lets you click "sync." One ArgoCD instance can manage dozens of registered clusters, which is why it became the default for teams that want developers to see their own deploys.

Flux thinks in controllers. There's no central anything. Inside each cluster you get source-controller (fetches git/Helm/OCI sources), kustomize-controller (applies Kustomize overlays), helm-controller (runs Helm releases), and notification-controller (alerts and webhooks). You compose them with CRDs:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/acme/apps.git
  ref:
    branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: ledger-api
  namespace: flux-system
spec:
  interval: 5m
  path: ./apps/ledger-api/overlays/prod
  prune: true
  sourceRef:
    kind: GitRepository
    name: apps

That's the whole model. No server, no UI, no sessions. Whether that's liberating or horrifying depends on who's asking.

Install day and upgrade day

ArgoCD's install is one Helm chart, but what lands is a real distributed system:

$ kubectl get pods -n argocd
NAME                                                READY   STATUS    RESTARTS   AGE
argocd-application-controller-0                     1/1     Running   0          3m
argocd-applicationset-controller-5f9f6c8f7d-q2xkz   1/1     Running   0          3m
argocd-dex-server-7d8b9c6d54-hwm4n                  1/1     Running   0          3m
argocd-notifications-controller-6c9f7b85b9-xv8t2    1/1     Running   0          3m
argocd-redis-75b4d6f7c9-kz5ql                       1/1     Running   0          3m
argocd-repo-server-6d8f5c9b7d-j9nrx                 1/1     Running   0          3m
argocd-server-5897d6b9b5-mx4qw                      1/1     Running   0          3m

Seven components, including dex for SSO and a repo-server that renders manifests and has historically been where your memory limits go to die. Upgrades are mostly fine, but major versions occasionally carry CRD changes or behavior shifts that deserve a staging run first.

Flux's install is one command, and it commits its own manifests to your repo, so the tool is itself GitOps-managed:

$ flux bootstrap github --owner=acme --repository=fleet --path=clusters/prod
► connecting to github.com
✔ repository cloned
► generating component manifests
✔ pushed sync manifests to "https://github.com/acme/fleet.git"
► installing components in "flux-system" namespace
✔ installed components
◎ verifying installation
✔ source-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ helm-controller: deployment ready
✔ notification-controller: deployment ready
✔ install finished

Four controllers, modest resource requests, and upgrades that amount to bumping manifest versions in git. In six weeks I never once thought about Flux's control plane. I thought about ArgoCD's roughly every other day. That's not a knock -- the UI and API are what you're paying for -- but the cost is real.

Day two: drift and diffs

Here's the difference that actually affects your incident count. Flux corrects drift by default. Every Kustomization reconciles on its interval with server-side apply and prune: true, so if someone hot-fixes a deployment with kubectl edit, Flux stomps it within five minutes and the git history stays the truth.

ArgoCD, out of the box, only *reports* drift. The app goes OutOfSync and stays that way until a human syncs it or you've opted into automated sync with selfHeal: true and prune: true. Half the "ArgoCD didn't deploy my change" support tickets I've seen trace back to a manual edit masking real drift that nobody looked at.

Both have a pre-flight diff, and both are worth wiring into CI:

$ flux diff kustomization ledger-api --path ./apps/ledger-api/overlays/prod
► Deployment/payments/ledger-api drifted
spec.template.spec.containers.ledger-api.image
  ± value change
    - registry.acme.com/ledger-api:1.19.2
    + registry.acme.com/ledger-api:1.20.0
$ argocd app diff ledger-api

ArgoCD's diff is nicer to read for big trees; Flux's is easier to script. When I want to see what the reconciler *thinks* is happening rather than what git says, I look at the cluster directly -- watching the live state of the namespace in conndeck next to the diff output is how I caught a Flux prune that was about to delete a hand-created ConfigMap.

Multi-tenancy and RBAC

ArgoCD ships its own authorization layer: AppProjects scope which repos, clusters, and namespaces a team can touch, and dex wires it to your IdP. It's genuinely good, and it's also a second RBAC system you now administer alongside Kubernetes RBAC. I've debugged more than one "works in the cluster, forbidden in the UI" mismatch.

Flux has no auth layer at all and that's deliberate. Tenancy is plain Kubernetes RBAC, and a Kustomization can impersonate a service account so tenant deploys run with tenant permissions:

spec:
  serviceAccountName: payments-deployer

If your org already thinks in namespaces and RBAC, this is refreshingly boring. If your org thinks in SSO groups and audit dashboards, ArgoCD hands you more out of the box.

So which one

Pick ArgoCD if developers need to see and sync their own apps, you want one control plane across many clusters, and ApplicationSets for fan-out matter to you. Accept that you're adopting a product with its own control plane to operate.

Pick Flux if you run clusters in the dozens, your platform team prefers composing CRDs over operating a server, you want drift correction on by default, and built-in SOPS support appeals. Accept that "no UI" is a real cost for non-platform people -- there are dashboards, but none are the product.

And if someone insists one is objectively superior, they either haven't run the other one, or they're selling something. The migration between them is mostly a workflow problem, not a YAML problem, so choose for your org chart, not the feature list.

Frequently asked questions

Is ArgoCD or Flux better for a small team?

For most small teams I'd pick Flux, because the footprint is four controllers, upgrades are uneventful, and you never operate an extra API server or SSO layer. The catch is losing the UI, and the UI is genuinely useful for developers who want to see why their deploy is red. If your team already lives in the ArgoCD UI, that value is real, so don't switch just to save some RAM.

Can ArgoCD and Flux manage multiple clusters from one place?

ArgoCD does this natively: one instance registers many clusters and gives you a single pane of glass. Flux is designed to run inside each cluster, one bootstrap per cluster, which scales fine but means N sets of controllers to upgrade and no built-in central view. Running Flux centrally against remote clusters is possible, but it's fighting the tool's grain.

Which handles secrets better, ArgoCD or Flux?

Flux has first-class SOPS integration in the kustomize-controller, so decryption happens natively during reconciliation with an age or KMS key. ArgoCD traditionally needed plugins like argocd-vault-plugin or an external operator. Honestly, I'd put External Secrets Operator in front of either one and keep ciphertext out of git entirely, which makes this difference mostly moot.

Does ArgoCD or Flux handle drift correction better?

Flux corrects drift continuously by default, because every Kustomization reconciles on an interval with server-side apply and prune, so manual edits get stomped on the next tick. ArgoCD can do the same thing, but only if you enable automated sync with selfHeal and prune per Application; out of the box it just reports OutOfSync. Flux's default is the safer one, and ArgoCD's opt-in model causes most of the 'why is my manual change still there' confusion.

How hard is it to migrate from ArgoCD to Flux or vice versa?

The manifests migrate easily, because both tools ultimately apply plain Kubernetes YAML, Kustomize, or Helm. What doesn't migrate is the operational layer: Application definitions become GitRepository plus Kustomization pairs, AppProjects become native RBAC, and your developers' UI habits need retraining. Budget for the workflow change, not the YAML.

The takeaway

ArgoCD is a product you operate; Flux is a toolkit you compose. That one sentence predicts almost every downstream difference -- install weight, upgrades, RBAC, UI, multi-cluster. Decide which of those two shapes fits your team and you'll pick the right tool.

Either way, wire the diff into CI, turn on drift correction deliberately, and remember the YAML was never the hard part.