Helm vs Kustomize: An Honest Take From Both Sides
The longest code review of my career was a values.yaml file. It was February, the payments team had forked their Helm chart for the third time, and I was staring at 1,147 lines of YAML where maybe sixty lines ever differed from the defaults. Somewhere around line 800, a nested conditional set a replica count that three overlays then overrode. I closed the review, opened a kustomization.yaml, and started deleting.
I've also shipped the opposite mistake -- a Kustomize setup with nine overlay layers where a Helm chart with defaults would have been one file. So this isn't a cheerleading post for either camp. It's what I've learned from running both, including the failure modes each side pretends don't exist.
Quick answer: Helm is a package manager: Go templates plus values files, plus release state stored in the cluster so you get history, upgrades, and rollbacks. Kustomize is a patch engine built into kubectl (kubectl apply -k): real YAML bases, structured patches, no variables, no release state. The honest default in 2026 is Helm for third-party software you don't own, Kustomize overlays for your own apps and environments -- and, under GitOps, the reconciler supplies the release management Kustomize never had.
What they actually are
This gets muddied constantly, so: Helm renders text. Templates are Go-template strings with {{ .Values.replicaCount }} sprinkled through them, and the output only becomes YAML after rendering. That's why a typo in a template produces a five-line stack trace instead of a schema error.
Kustomize patches structure. Everything in a kustomization is already valid Kubernetes YAML. Overlays apply strategic-merge or JSON patches against that structure:
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payments
resources:
- ../../base
images:
- name: registry.acme.com/ledger-api
newTag: "1.20.0"
patches:
- path: patch-replicas.yaml
- path: patch-resources.yaml
No conditionals, no loops, no templating language to debug. Also no variables -- if you need "if production then three replicas," you write a prod overlay. People call that a limitation. After the 1,147-line values file, I call it a feature.
Helm's real feature is release management
Everyone argues about templating, but the thing Helm has that Kustomize fundamentally doesn't is *state*. Every install records a revision, stored as Secrets in the namespace:
$ helm list -n payments
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
ledger-api payments 42 2026-08-20 14:02:11 +0200 CEST deployed ledger-api-1.19.3 1.19.3
redis payments 3 2026-07-02 09:11:03 +0200 CEST deployed redis-19.6.4 7.2.5
$ helm history ledger-api -n payments
REVISION UPDATED STATUS DESCRIPTION
40 2026-08-06 10:14:02 superseded Upgrade complete
41 2026-08-13 10:11:47 superseded Upgrade complete
42 2026-08-20 14:02:11 deployed Upgrade complete
helm upgrade --install, helm rollback, diff plugins against the last release -- this is a real deployment toolchain. The catch is that Helm only manages things *during an upgrade*. Between upgrades, the cluster can drift from the release state and Helm neither knows nor cares. The other catch is helm rollback: it re-applies old manifests, but it doesn't rewind CRDs, data, or anything stateful. Treat it as "redeploy the old YAML," not "undo."
And then there's the values sprawl. Public charts expose hundreds of knobs, teams override twelve of them, and six months later nobody can tell you which values are load-bearing. If you've ever diffed two environment values files to figure out why staging behaves differently, you know the feeling.
Kustomize's costs are quieter
Kustomize's weaknesses don't announce themselves in code review. They show up later:
No release history. kubectl apply -k is stateless. There's no revision 42 to roll back to. If Kustomize is your whole toolchain, your rollback strategy is git revert -- which is fine, actually great, *if* a reconciler applies the result, and terrifying if it relies on someone remembering to re-apply by hand.
The hash suffix surprise. configMapGenerator appends a content hash to the name (app-config-9t2k5b7m48) and rewrites references, so pods roll when config changes. Wonderful behavior. Also the cause of the classic "why does my ConfigMap have a different name than the file" ticket, and of orphaned ConfigMaps piling up if you apply without prune.
Silent patch failures. A patch targeting the wrong name: or kind: can fail in ways that are easy to miss in CI unless you build the overlay and diff it. kustomize build overlays/prod | diff - cluster-state catches what eyeballs don't.
What GitOps changes about this fight
Here's the thing that makes most "Helm vs Kustomize" arguments feel dated: if ArgoCD or Flux applies your manifests, the reconciler *is* your release management. Git is the revision history, a revert is the rollback, drift detection is continuous, and Helm's cluster-side state becomes mostly redundant bookkeeping.
That's why I increasingly see, and run, this split: Helm charts for software I don't own (cert-manager, ingress-nginx, postgres operators), rendered by Flux's helm-controller or ArgoCD's Helm support. Kustomize overlays for everything we wrote ourselves, where a base plus two or three overlays beats maintaining a chart nobody else will ever install. When I need to sanity-check which one produced a given resource on a live cluster, I just look at the object directly -- conndeck shows the applied manifest and labels inline, which settles "did Helm or the overlay set this" in about two seconds.
Using both without hating yourself
The combination pattern that works:
helm-charts/redis/ # values only, no forks
apps/ledger-api/
base/ # plain YAML, no placeholders
overlays/
staging/kustomization.yaml
prod/kustomization.yaml
Third-party charts get rendered with helm template (or the controller equivalent) into a base, then your overlay patches what the values file can't express -- Flux does this natively with postRenderers on a HelmRelease. Your own apps never become charts. The moment you write {{ if .Values.env }} in your own app's templates, ask yourself why an overlay wouldn't say it more clearly.
Two rules that have saved me repeatedly: never fork a public chart (values or post-render patches, always), and never let a chart you own grow a second consumer -- that's when templating metastasizes.
Frequently asked questions
Should I use Helm or Kustomize for my own applications?
For your own apps, Kustomize is usually enough and often better. You own the manifests, so you don't need a templating language -- a base plus per-environment overlays with patches covers replicas, resources, and image tags without inventing a values file. Reach for Helm when you need to package something for other people to install, or when you genuinely need conditionals and loops in the rendering.
Can I use Helm and Kustomize together?
Yes, and it's one of the most common real-world setups. The standard pattern is rendering a third-party chart with helm template into a directory, then applying a Kustomize overlay on top for your environment-specific tweaks. Flux supports this natively with postRenderers on a HelmRelease, and ArgoCD lets an Application combine a Helm source with Kustomize patches.
Does Kustomize support variables or templating?
No, and that's a deliberate design decision. Kustomize works with structured patches against real YAML, not string substitution, so there's no if/else, no loops, and no values file. What you get instead are transformers like images, replicas, and configMapGenerator, plus patches. If you find yourself fighting the lack of conditionals, that app probably wants Helm or a different structure of overlays.
How do Helm rollbacks actually work?
helm rollback re-applies the manifest set from a previous revision, using the release state Helm stores in Secrets in the target namespace. It is not a time machine: it doesn't undo CRD changes, restore deleted data, or rewind external state like migrated databases. It's better than nothing and worse than a GitOps revert, where rolling back means reverting a commit and letting the reconciler converge.
Why does Kustomize rename my ConfigMaps with a hash suffix?
Because configMapGenerator appends a content hash to the generated name and updates every reference to it, which is how pods get restarted automatically when config changes. A Deployment referencing the old name rolls to the new one on the next apply. If the churn bothers you, set disableNameSuffixHash: true on the generator, but then you're back to restarting things by hand when config changes.
The practical version
Helm is a package manager with release state; Kustomize is a patch engine with none. Most teams need both, pointed at different problems: charts for what you don't own, overlays for what you do.
And if a GitOps reconciler applies everything anyway, half of what you were arguing about is already solved by git.