Where does the variation live?
Helm moves the differences out of the manifests and into values. A chart is "a collection of files inside of a directory": Chart.yaml, a values.yaml with defaults, an optional values.schema.json, and a templates/ directory that, "when combined with values, will generate valid Kubernetes manifest files". The templates are Go templates, so the source files are not manifests until rendered, and the same chart can express loops, conditionals and helper functions that a static overlay cannot.
Kustomize refuses that move on principle. Its maintainers keep a list of eschewed features, and parameterization is on it: the source YAML "gets polluted with $VARs" and can no longer be applied as is to the cluster. The base stays valid YAML you could apply unmodified; an overlay names the base under resources and adds patches, generators and transformers. The project describes its own boundary as "YAML in / YAML out" and points anyone who wants a configuration language at other tools. There are no removal directives either: an overlay can add labels, patches and resources but cannot subtract, and the documented answer to "the base has something I do not want" is to fork the base.
This is the first place the choice gets made. If the people who write the manifests and the people who deploy them are the same team, valid YAML with small patches is easier to review and harder to break. If a chart has to be handed to strangers who will install it in clusters you will never see, template plus values is the interface they expect, and values.schema.json lets you reject bad inputs before anything is rendered.
What each one leaves behind in the cluster
Helm keeps state. "By default, release information is stored in Secrets in the namespace of the release", switchable to ConfigMaps or SQL through HELM_DRIVER, and the same page notes that those records "include the contents of charts and values files, and therefore might contain sensitive data". Every upgrade adds a revision; helm upgrade keeps 10 by default (--history-max), and helm rollback takes a release name and a revision number, or with no revision "will roll back to the previous release". That history is the feature: an operator who has never seen the chart can list revisions and go back one.
Kustomize leaves nothing of its own. kubectl apply -k applies the rendered objects and that is the whole record; there is no release, no revision list, and no rollback command. The rollback story is Git: revert the overlay change and apply again. The cluster-side tooling that Kustomize does give you is comparison: kubectl diff -k shows how the live objects would change before you apply.
# Helm: values-prod.yaml, consumed by templates you cannot see from herereplicaCount: 3image:tag: "2.4.1"resources:limits:memory: 512Mi# Kustomize: overlays/prod/kustomization.yaml, a patch on YAML you can readapiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- ../../baseimages:- name: registry.example.com/shop/apinewTag: "2.4.1"patches:- path: replicas.yaml # replicas: 3, memory limit 512MiconfigMapGenerator:- name: api-configfiles:- config.properties # name gets a content-hash suffix
The last three lines of the Kustomize file change behaviour, not just style. Generated ConfigMaps and Secrets "have a content hash suffix appended", so a change to config.properties produces a new object name and every Deployment that references it rolls; disable it with generatorOptions.disableNameSuffixHash if that is not what you want. A Helm chart that mounts a ConfigMap by a fixed name updates the object in place and the pods keep the old data until something restarts them, unless the chart author added a checksum annotation. Both behaviours are reasonable; only one of them happens without a chart author remembering to make it happen.
Rendering, dry runs and what a pipeline can catch
Both tools render to stdout, which is how you review them in CI. helm template renders "locally", which the docs qualify twice: "any values that would normally be looked up or retrieved in-cluster will be faked locally", and "none of the server-side testing of chart validity (e.g. whether an API is supported) is done". For a real check you pass --dry-run=server on install or upgrade, which requires cluster connectivity. kubectl kustomize renders the overlay with no cluster at all, and the rendered stream is plain objects you can hand to any validator or scanner.
helm template shop ./chart -f values-prod.yaml --api-versions networking.k8s.io/v1 | head -n 6---# Source: chart/templates/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: shop-apikubectl kustomize overlays/prod | grep -n "name: api-config"12: name: api-config-8mbdf7882g41: name: api-config-8mbdf7882gkubectl diff -k overlays/prod- image: registry.example.com/shop/api:2.4.0+ image: registry.example.com/shop/api:2.4.1Two different pictures of the same objects. Helm gives you a rendered document per template file with a source comment; Kustomize gives you the assembled objects with the generated names already substituted into every reference. If your pipeline gate is "scan what will actually be applied", both satisfy it, but the Kustomize output is the whole truth and the Helm output is the truth minus whatever the chart looks up in-cluster at install time.
Distribution: packaged charts versus Git refs
Helm has a supply chain. Charts are packaged, versioned with semver in Chart.yaml, published to repositories or, as the docs now recommend, to OCI registries with helm push; Helm 4 can install by digest, and "charts with non-matching digests are not installed". Dependencies are declared in Chart.yaml with version constraints and conditions, and library charts exist for sharing template helpers without shipping resources. All of this is what makes a chart installable by someone who has never read it.
Kustomize has Git. A resources entry can point at a remote repository path with a ref, and kubectl kustomize accepts a GitHub URL with ?ref=... directly. There is no package, no index and no version constraint solver; the pin is the Git ref you wrote, and the trust question is whether the remote repository is one you control. The load restrictor defaults to LoadRestrictionsRootOnly, so an overlay cannot read files outside its own root unless you loosen it and accept that the kustomization stops being relocatable.
Version skew is worth stating plainly because it surprises teams. Helm 4 is "assumed to be compatible with n-3 versions of Kubernetes it was compiled against", and the project maintains a release branch for the most recent minor. Kustomize ships two ways: the standalone binary (v5.8.1 at verification) and the copy embedded in kubectl, which is pinned per kubectl release and can lag the standalone tool. A kustomization.yaml that uses a new field may build with kustomize and fail with kubectl apply -k on an older client; pin one of them in CI and use it everywhere.
CRDs, server-side apply and the upgrade nobody plans for
This is the sharpest documented difference. Helm treats CRDs as a special case: files in a chart's crds/ directory are installed before templates, but "CRDs are never reinstalled" and "CRDs are never installed on upgrade or rollback". The best-practice guide is explicit that this "was an explicit decision after much community discussion due to the danger for unintentional data loss". Operationally it means a chart bump that needs a new CRD version does not apply it; you ship CRDs as their own chart or apply them out of band, and a Kustomize overlay that includes the CRD manifest simply updates it, with all the data-loss risk that Helm was avoiding on your behalf.
Helm 4 also changed how objects are written. New releases default to server-side apply; upgrades "follow the previous apply method of the release", so "all releases created by Helm 3 will default to using client-side apply after upgrading to Helm 4" unless you pass --server-side explicitly. kubectl apply -k follows whatever kubectl apply does, with --server-side available the same way. If controllers or other tools also manage fields on the same objects, this is where conflicts show up, and it is the same conflict on both sides once Helm is on SSA.
Using both: the two hybrids and what each gives up
Hybrid one is Kustomize inflating a chart. The helmCharts field "has limited support for helm chart inflation", requires kustomize build --enable-helm (or kubectl kustomize --enable-helm) and a helm binary on the path, and the support policy is narrow on purpose: bug fixes, security fixes, and "additional fields that are analogous to flags passed to helm template, except for flags such as post-renderer that allow arbitrary commands to be executed". The project states it "will not add support for private repository or registry authentication". The generator renders the chart to plain objects, at which point your patches apply; Helm never creates a release, so helm ls, history and rollback do not exist for that application.
Hybrid two is Helm calling Kustomize. Post-rendering lets you "use tools like kustomize to apply configuration changes without the need to fork a public chart", keeping the Helm release and history intact. Helm 4 turned this into a plugin mechanism: "it is no longer possible to pass an executable directly" to --post-renderer; a plugin name is required. Helm 4 also sends chart hooks to the post-renderer by default (the "combined" strategy, a change from Helm 3); the "nohooks" strategy that restores Helm 3 behaviour is set through the Go SDK, not a CLI flag, and the docs point Kustomize users at it when patches target template-only resources. The cost is a plugin to install everywhere Helm runs, and a chart whose rendered output no longer matches what its author tested.
Pick the hybrid by which record you want to keep. If the application should show up in helm history and roll back with helm rollback, post-render. If the application should be one more directory in a Git tree that a GitOps controller applies, inflate. The mechanics of the inflation pattern, with the kustomization and patch files written out, are in the Kustomize and Helm together lesson.
Where each one belongs
Which tool, by situation
| Situation | Use | Why | What it costs you |
|---|---|---|---|
| Installing vendor software (ingress, database operator, observability stack) that the vendor publishes as a chart | Helm | The chart is the supported interface; values.schema.json and digest pinning protect you from bad inputs and swapped artifacts | CRDs in crds/ are never upgraded by Helm; plan a separate CRD step for every operator upgrade |
| Your own services, one Git repository, three environments, one team owning both code and manifests | Kustomize | The base is reviewable YAML, overlays hold only the differences, and generated config names roll pods on change | No release history; rollback is git revert plus apply, so the Git history has to be clean enough to revert |
| Publishing an application other teams or customers will install without talking to you | Helm | Packaging, semver, dependencies, OCI distribution and NOTES.txt exist for exactly this | You now maintain a templating surface; every values key you expose is an interface you cannot easily remove |
| A vendor chart that needs org-wide changes the chart does not expose (labels, sidecars, a hardened securityContext) | Helm with a post-renderer plugin | Keeps the release record and rollback while applying patches after rendering | A plugin to install everywhere Helm runs, chart hooks that now pass through the post-renderer too, and rendered output the chart author never tested |
| GitOps repository where every application must be a plain directory the controller renders, vendor charts included | Kustomize with helmCharts inflation | One rendering path, one review process, patches on top of any chart | No private registry auth in the generator, --enable-helm and a helm binary wherever builds run, and no helm history for the app |
| Operators and cluster add-ons whose CRDs change every release | Kustomize for the CRDs, either tool for the workload | Kustomize applies CRD updates as ordinary resources; Helm will not touch them after first install | You own the data-loss review Helm was refusing to automate; diff CRD schema changes before applying |
The trade-offs, priced
What you buy and what it costs
| Choice | Benefit | Cost | Consequence, who should care |
|---|---|---|---|
| Templates and values (Helm) | One artifact serves any consumer; conditionals and helpers handle real variation | Source files are not manifests until rendered; whitespace and template errors are found at render time | Reviews happen on rendered output, not on the diff. Platform teams shipping to many consumers should care most. |
| Patches on plain YAML (Kustomize) | Every file in Git is applicable as-is and readable without a renderer | No parameterization, no removal directives; awkward variation ends up as a forked base | Small diffs can hide large rendered changes; the pipeline must build and scan the overlay. Application teams owning their own manifests should care most. |
| Release records in the cluster (Helm) | History, helm rollback, and an operator can act without the source | Secrets hold chart and values contents; history grows until --history-max trims it | Read access to release Secrets is read access to values. Anyone doing RBAC reviews should care. |
| No cluster state (Kustomize) | Nothing to migrate, back up or clean; kubectl diff -k is the whole preview | Nothing to roll back to except Git | Git discipline becomes the recovery plan. Teams without protected branches and clean history should care. |
| Chart packaging and OCI distribution | Semver, dependencies, digest verification, registries you already run | A packaging step and a registry to operate | Supply-chain controls apply to charts as to images. Security teams should care. |
| CRD caution (Helm) versus CRD updates (Kustomize) | Helm will not destroy CRD-backed data on upgrade; Kustomize keeps CRDs current | Helm leaves CRD upgrades to you; Kustomize leaves the data-loss judgement to you | Someone has to own CRD lifecycle explicitly in either model. Operators of CRD-heavy add-ons should care. |
A decision rule in three questions
First: who consumes the output? If people outside your team install it, package it as a chart; nothing on the Kustomize side replaces semver, schema validation and digest pinning for strangers. If only your team applies it, start with Kustomize and add Helm only when you hit variation that patches cannot express.
Second: which record do you want during an incident? If the answer is "helm history, then helm rollback", keep Helm in the loop even for vendor charts you patch, and use a post-renderer plugin rather than inflation. If the answer is "the Git log", inflation into a Kustomize tree is fine and simpler to reason about, provided the loss of helm history is a decision and not an accident.
Third: who owns CRDs? Neither tool will own them for you. Helm documents that it will not upgrade them; Kustomize will upgrade them without judgement. Write down which team applies CRD changes and how they are diffed before anyone chooses a tool for the workloads that depend on them. Both projects are actively released (Helm 4.3.0 in September 2026, Kustomize 5.8.1 in February 2026). Answer the three questions first; the tool usually falls out of them.