Mert Özen Further With Every Line
DevOps Helm — #07

From Helm 3 to Helm 4: What Changed, What Breaks and the Helm 3 EOL Timeline

Mert Özen Nov 15, 2026 12 min 6 views
From Helm 3 to Helm 4: What Changed, What Breaks and the Helm 3 EOL Timeline

Helm 4 manages Helm 3 charts and releases unchanged; what breaks are scripts, plugins and post-renderers, and Helm 3's security support ends on February 10, 2027. We run Helm 3.22 and Helm 4.3 side by side and look with real output at server-side apply latching, how mixing the two versions undoes server-side apply, flag and log changes, post-renderers becoming plugins, the experimental Chart v3 and a Helm 2 era chart, and finish with a migration checklist.

Version note: The examples in this post were run with Helm 4.3.2 and Helm 3.22.1, kind 0.33 with Kubernetes 1.37, and kubectl 1.37.

Guess First

You installed a release with Helm 3. You upgrade CI's Helm to version 4 and upgrade the same release. Helm 4 installs new releases with server-side apply; does it update this old release with server-side apply too? If someone on the team still runs an upgrade on the same release with Helm 3 on their own machine, which method does the next Helm 4 upgrade use? The answer is in the server-side apply section, with real values read from inside the release Secret.

The Timeline: the End of Helm 3

Helm 4.0.0 was released on November 12, 2025. The Helm team first announced that bug fixes for Helm 3 would end in July 2026 and security fixes in November 2026. With the end-of-life announcement in June 2026 the timeline was extended:

DateEvent
November 12, 2025Helm 4.0.0 released
September 9, 2026Planned as Helm 3's last, limited feature release (3.22.0 on GitHub, dated September 10); Helm 4.3.0 came out the same week
Until September 9, 2026Bug fixes for Helm 3
October 15, 20263.22.1 patch release (4.3.1 on the same day)
February 10, 2027End of security fixes for Helm 3; after this date no Helm 3 release of any kind will be made

3.22 is only a client library update for new Kubernetes versions; no new features come to Helm 3. After February 2027, Helm 3 will not be tested against new Kubernetes versions, and a security hole will not be fixed even if one is found. On the day this post was written, the current Helm 4 version is 4.3.2 (November 13, 2026).

What Changed

The Helm team's goal was that charts installable with Helm 3 stay installable with Helm 4, and that Helm 3 releases can be managed by Helm 4 without a migration step; in this series' experiments the charts also worked unchanged. What breaks is mostly around the charts: CI scripts, plugins, tools that use the SDK. The short list:

  • Server-side apply: the default for new releases, existing releases stay on their previous method.
  • Waiting with kstatus: --wait takes a strategy (watcher, hookOnly, legacy); without the flag only hooks are waited on.
  • A new plugin system: apiVersion, type and runtime fields in plugin.yaml, an optional WASM-based runtime, post-renderers as plugins.
  • Flag changes: --rollback-on-failure instead of --atomic, --force-replace instead of --force; helm list --all was removed and the list shows every status by default; helm status shows resources by default.
  • helm registry login accepts only a registry address; installing OCI charts by digest.
  • Values: multi-document values files and a whole JSON object through --set-json.
  • Logging moved to Go's slog package; the SDK's import path is helm.sh/helm/v4 and the packages are versioned (pkg/chart/v2, pkg/release/v1).
  • The experimental Chart v3 (HELM_EXPERIMENTAL_CHART_V3), reproducible chart packages and a content-based cache.

We saw some of these in the earlier posts of the series: kstatus and --rollback-on-failure in the fifth, registry login and digests in the sixth, multi-document values in the fourth. In this post we try the rest by running the two Helm versions side by side. We installed Helm 3.22.1 into a separate folder and call it as helm3:

helm3 version --short
helm version --short
v3.22.1+gd6c1a0e
v4.3.2+g8346aaa

Server-Side Apply and Latching

We installed the chart of Bookstore, the online bookstore used as the shared example across this site's series, with Helm 3, and read the apply_method field in the release Secret and the operation type of the helm manager on the Deployment with two helper functions:

helm3 install bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml --wait
# The apply_method field in the release Secret and helm's managedFields entry on the Deployment
release() {
    kubectl -n bookstore-legacy get secret "sh.helm.release.v1.bookstore.v$1" -o jsonpath='{.data.release}' \
        | base64 -d | base64 -d | gunzip | jq -c '{version, apply_method: (.apply_method // "(none)")}'
}
owner() {
    kubectl -n bookstore-legacy get deploy bookstore-api -o json --show-managed-fields \
        | jq -r '.metadata.managedFields[] | select(.manager=="helm") | "\(.manager)\t\(.operation)"'
}
release 1; owner
{"version":1,"apply_method":"(none)"}
helm	Update

Helm 3 never writes this field and makes changes with client-side apply; the Update in managedFields shows that. Now we upgraded the same release with Helm 4, first without a flag, then with --server-side=true:

helm upgrade bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml --wait
release 2; owner
helm upgrade bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml --server-side=true --wait
release 3; owner
{"version":2,"apply_method":"csa"}
helm	Update
{"version":3,"apply_method":"ssa"}
helm	Apply

In the upgrade without a flag, Helm 4 "latched" onto the release's previous method and continued with client-side apply, and wrote that into the record as csa. With --server-side=true it switched to server-side apply, Apply appeared in managedFields, and later upgrades without a flag will continue with server-side apply. The second half of Guess First:

helm3 upgrade bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml --wait
release 4
helm upgrade bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml
release 5
{"version":4,"apply_method":"(none)"}
{"version":5,"apply_method":"csa"}

The upgrade Helm 3 made dropped the apply_method field from the record because it does not know it; the Helm 4 upgrade that followed did not find the field and tied the release to client-side apply again. So touching the same release with two Helm versions silently undoes the move to server-side apply. That is why the whole team and every pipeline must move to Helm 4 at the same time. Moving to server-side apply also brings the field ownership conflicts we saw in the fifth post: fields changed by hand with kubectl are no longer silently overwritten, and the upgrade stops with a conflict error. Make the switch at a time when there are no such manual changes, and in staging first.

Flags, Commands and Logs

helm upgrade bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml --force
helm list --all -n bookstore-legacy
Flag --force has been deprecated, use --force-replace instead
Release "bookstore" has been upgraded. Happy Helming!
Error: unknown flag: --all

Renamed flags (--atomic, --force) keep working with a warning, and searching CI logs for these lines is the first step of the migration. Removed flags break the script: helm list --all is an unknown flag in Helm 4, because helm list now shows every status by default. Addresses with a path, such as helm registry login registry.example.com/charts, which worked in Helm 3, fail as we saw in the sixth post. The logs changed too: Helm 4 uses Go's structured logging package slog, and warnings and debug lines come in the level=... msg=... form. If you have a log collection rule that parses these lines, you need to update it:

helm upgrade bookstore charts/bookstore -n bookstore-legacy -f deploy/values-dev.yaml --debug 2>&1 | grep "^level=" | head -4
level=DEBUG msg="preparing upgrade" name=bookstore
level=DEBUG msg="getting last revision" name=bookstore
level=DEBUG msg="getting release history" name=bookstore
level=DEBUG msg="chart name" chart-name=bookstore

There is a behaviour change as well: an upgrade without --wait does not wait for Deployments to become ready in Helm 4 either, and the default hookOnly strategy waits only for hooks. Scripts that used --wait in Helm 3 move to the watcher strategy in Helm 4, and the waiting logic relies on kstatus; the timeout messages are different too.

The Plugin System and Post-Renderers

A post-renderer is a program Helm passes the rendered manifests through before sending them to the cluster. In Helm 3 this program could be an executable given directly. With a small script that adds a team label to every resource:

#!/usr/bin/env sh
# Post-renderer: reads the rendered manifests on stdin, writes them to stdout with a team label.
sed 's/^  labels:$/  labels:\n    example.com\/team: bookstore/'
helm3 template bookstore charts/bookstore -f deploy/values-dev.yaml \
    --post-renderer ./add-team-label.sh --show-only templates/service.yaml | grep -A2 "labels:"
helm template bookstore charts/bookstore -f deploy/values-dev.yaml --post-renderer ./add-team-label.sh
  labels:
    example.com/team: bookstore
    app.kubernetes.io/name: bookstore
Error: invalid argument "./add-team-label.sh" for "--post-renderer" flag: plugin: {Name:./add-team-label.sh Type:postrenderer/v1} not found

Helm 3 ran the script, while Helm 4 took ./add-team-label.sh for a plugin name and could not find it. In Helm 4 a post-renderer is a plugin type; the script has to be wrapped with a plugin.yaml:

apiVersion: v1
type: postrenderer/v1
name: team-label
version: 0.1.0
runtime: subprocess
runtimeConfig:
  platformCommand:
    - command: ${HELM_PLUGIN_DIR}/add-team-label.sh
helm plugin install ./plugins/team-label
helm plugin list
helm template bookstore charts/bookstore -f deploy/values-dev.yaml --post-renderer team-label \
    --show-only templates/service.yaml | grep -A2 "labels:"
Installing plugin from local directory (development mode)
Installed plugin: team-label
NAME      	VERSION	TYPE           	APIVERSION	PROVENANCE	SOURCE
team-label	0.1.0  	postrenderer/v1	v1        	local dev 	unknown
  labels:
    example.com/team: bookstore
    app.kubernetes.io/name: bookstore

A plugin installed from a local folder is installed in "development mode", without a provenance check. In Helm 4, signing and verification for plugins installed from remote sources also became part of the plugin system. We saw a side effect of the new system in Helm 3: when the two versions share the same plugin folder, Helm 3 cannot read the new-format plugin.yaml and prints a warning on every command:

helm3 template x v3demo
failed to load plugins: failed to load plugin at "<HELM_DATA_HOME>/plugins/team-label/plugin.yaml": error unmarshaling JSON: while decoding JSON: json: unknown field "apiVersion"

Existing Helm 3 plugins keep working in Helm 4; we saw no problems with the charts in this series, but you should test the plugins you write or use with Helm 4 separately. For plugin maintainers, the Helm documentation has a migration guide that lists the new equivalents of the old fields. The WASM runtime (runtime: extism/v1) runs the plugin in a sandbox, with allowed hosts and a memory limit; I did not try this runtime for this post.

Chart Formats: v1, v2 and the Experimental v3

Helm 4 lays the groundwork for a new chart format. Chart v3 sits behind an experimental flag:

helm create --chart-api-version=v3 v3demo
HELM_EXPERIMENTAL_CHART_V3=1 helm create --chart-api-version=v3 v3demo
head -2 v3demo/Chart.yaml
HELM_EXPERIMENTAL_CHART_V3=1 helm lint v3demo
HELM_EXPERIMENTAL_CHART_V3=1 helm install x v3demo --dry-run=client
Error: this feature has been marked as experimental and is not enabled by default. Please set HELM_EXPERIMENTAL_CHART_V3=1 in your environment to use this feature
Creating v3demo
apiVersion: v3
name: v3demo
==> Linting v3demo
[ERROR] Chart.yaml: apiVersion 'v3' is not valid. The value must be either "v1" or "v2"
[INFO] Chart.yaml: icon is recommended
Error: INSTALLATION FAILED: invalid chart apiVersion

In 4.3.2 a v3 chart skeleton can be created and read with helm show chart, but lint and install do not accept it yet. The charts you write today should stay on apiVersion: v2; v2 charts work unchanged in Helm 4.

At the other end are charts from the Helm 2 era. Helm 2 had a server component in the cluster called Tiller, and its support ended in November 2020; but charts from that period still turn up in old repos and internal repositories. The signs to recognise them: apiVersion: v1, a separate requirements.yaml for dependencies, and often Kubernetes APIs that no longer exist. We tried such a chart with Helm 4:

# Chart.yaml (Helm 2 era)
apiVersion: v1
name: bookstore-legacy
version: 0.9.0
appVersion: "0.9"
description: A Helm 2 era chart (apiVersion v1)
---
# requirements.yaml: dependencies live here in v1 charts
dependencies:
  - name: valkey
    version: "1.0.x"
    repository: https://valkey.io/valkey-helm/
    condition: valkey.enabled
---
# templates/deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-api
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}-api
    spec:
      containers:
        - name: api
          image: {{ .Values.image }}
helm dependency list legacy-chart
helm lint legacy-chart
NAME  	VERSION	REPOSITORY                    	STATUS
valkey	1.0.x  	https://valkey.io/valkey-helm/	missing

==> Linting legacy-chart
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/deployment.yaml: extensions/v1beta1 Deployment is deprecated in v1.8+, unavailable in v1.16+; use apps/v1 Deployment
[ERROR] templates/deployment.yaml: a Deployment must contain matchLabels or matchExpressions, and "test-release-api" does not
[WARNING] legacy-chart: chart directory is missing these dependencies: valkey
Error: 1 chart(s) linted, 1 chart(s) failed

Helm 4 still reads the v1 format: it recognised the dependency in requirements.yaml, and helm dependency update wrote requirements.lock instead of Chart.lock. Lint showed the removed API as a warning and the Deployment without a selector as an error. The install came back from the API server:

helm dependency update legacy-chart
ls legacy-chart
helm install old legacy-chart -n bookstore-old
...
Deleting outdated charts
Chart.yaml
charts
requirements.lock
requirements.yaml
templates
values.yaml
Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: resource mapping not found for name: "old-api" namespace: "" from "": no matches for kind "Deployment" in version "extensions/v1beta1"
ensure CRDs are installed first

extensions/v1beta1 Deployments were removed in Kubernetes 1.16; the list of this and similar APIs is in the eighteenth post of the Kubernetes series. To migrate such a chart you first move to apiVersion: v2 and bring the dependencies into Chart.yaml, then update the API versions in the templates. If the cluster has a release recorded with a removed API (the manifest in the release Secret points to the old API), even an upgrade is impossible; for that, the Helm community's helm-mapkubeapis plugin rewrites the APIs in the release record. I did not run this plugin for this post.

Migration Checklist

  • Upgrade Helm to 4.x in CI images and on developer machines at the same time; touching the same release with two versions undoes the move to server-side apply.
  • Search scripts for --atomic, --force, helm list --all, helm registry login with a path, and --post-renderer given an executable directly.
  • Add a deliberate strategy to deploy steps that do not use --wait; Helm 4's default waits only for hooks.
  • Move post-renderers to postrenderer/v1 plugins and your own plugins to the new plugin.yaml format, and test them with Helm 4.
  • Move releases to server-side apply deliberately (--server-side=true), in staging first; expect fields changed by hand to cause conflicts.
  • Move internal tools that use the Helm SDK to helm.sh/helm/v4 and update slog-based log parsing rules.
  • Search old charts for removed Kubernetes APIs with helm lint and kubeconform; move apiVersion: v1 charts to v2.

In Production

Moving to Helm 4 looks like a binary change but is really a change of deploy method; put the migration into a release window and watch the first upgrades closely. The benefit of server-side apply is clearer in clusters where several tools touch the same resource: when Argo CD, operators, the HPA and Helm manage different fields of the same Deployment, conflicts become visible instead of being silently overwritten. Helm 3's security support ends on February 10, 2027; a security hole found after that stays in Helm 3. Pin the Helm version in CI (v4.3.2 in this series' workflow) and do upgrades through a pull request, with the chart tests running.

Common Pitfalls

Thinking every release moves to server-side apply by itself after switching to Helm 4. Moving releases to server-side apply while someone on the team keeps using Helm 3. Ignoring deprecation warnings and waiting for the flags to be removed in the next major version. Assuming a post-renderer script still works in Helm 4. Using Helm 3 and Helm 4 with the same plugin folder and getting plugin warnings all the time. Trying to use Chart v3 for production charts. Trying to install a Helm 2 era chart on a new cluster without looking at its removed APIs.

A Small Experiment

★☆☆ Search your own CI scripts and Makefiles for --atomic, --force, list --all and registry login, and replace them with their Helm 4 equivalents.

★★☆ Install a release with Helm 3, change a few fields with kubectl edit, and upgrade with Helm 4 first without a flag, then with --server-side=true. Note at which step you get a conflict error and how apply_method changes.

★★★ Rewrite the post-renderer plugin to work with Kustomize: the plugin writes the manifests coming from stdin to a file and applies a patch with kustomize build. Carry the overlay idea from the fourteenth post of the Kubernetes series onto the Helm chart this way.

With this post the Helm series is complete. At the end of the series you have a chart for Bookstore that is versioned, has a schema, runs its migration through a hook, is published to an OCI registry and is tested in CI. Syncing this chart from Git continuously, handing the deploy from a pipeline to an agent inside the cluster, is the topic of the GitOps and Argo CD series.