TRANSCODE Explorations into the Code Transcendental.

Helm's Big Index Problem

Twenty times its size in memory, and what helps

A classic Helm chart repository is a web server with one special file, index.yaml. It lists every version of every chart in the repository, with each version’s metadata, and it grows with every release. Helm reads all of it for helm repo add, repo update, pull, install, dependency update and the rest.

For a small repository that’s nothing. For a big one it’s seconds of CPU and gigabytes of memory, and it has been knocking over Helm, Argo CD and Flux for years. I ran into it while testing Oyl, my YAML library, against Helm. Here’s what I found.

What it costs

I built Helm from its current source and timed it against a local web server, with a clean Helm home each time, taking the median of three runs. I used two indexes: Bitnami’s, the biggest public one I know of at 27.6 MB, and a 64.5 MB one I merged from 19 public repositories with Helm’s own merge code, to stand in for the big private and virtual repositories companies run.

  Bitnami’s index (27.6 MB) merged index (64.5 MB)
helm repo add 1.19 s, 627 MiB 2.92 s, 1,268 MiB
helm pull from the added repo 1.17 s, 603 MiB 2.85 s, 1,415 MiB
helm pull --repo 2.34 s, 611 MiB 5.66 s, 1,502 MiB

The memory is peak RSS, and it’s the real problem: about twenty times the size of the index, for every helm process. Nothing is shared between two of them running side by side.

Who it hurts

  • Helm itself. In helm/helm#9931 (2021), helm repo add ran out of memory with a 65.56 MB index on a one-core, 2 GB machine. With 4 GB it worked, after a minute and 12 seconds. A Helm maintainer suspected YAML parsing, and called the Kubernetes parser Helm uses the one “I’ve seen that uses the most memory.”
  • Argo CD. Its repo server fetches charts with helm pull. In argoproj/argo-cd#23402, a company with 100 clusters, each running Argo CD with 300 to 800 applications, measured about 900 MB per helm pull against a 40 MB index. With 30 pulls allowed at once, that’s up to 27 GB for one repo server, by their arithmetic. Another organization in the thread has an index “upwards of 200 MB.”
  • OpenShift GitOps. Red Hat’s distribution of Argo CD doubled the repo server’s default memory limit in 2021, from 512 MiB to 1 GiB, after users reported it running out of memory. The issue doesn’t say what used the memory, but a 900 MB helm pull would do it.
  • Flux. Its source controller kept getting OOM-killed at 1 and 2 GB limits. A Flux maintainer explained that they hadn’t found a way to work with indexes “without loading them in memory in full (due to certain constraints around the unmarshalling of YAML),” and that Artifactory “likes to stuff as much as possible in a single index (in some cases resulting in a file of >100MB).” Flux’s source controller now refuses any index over about 50 MB unless you raise the limit.

All of these but the Argo CD issue are closed, and none was fixed. One was closed by a bot for going stale. The others were worked around with more memory, a cache or a higher limit, and parsing a YAML index is no cheaper for it.

Why it costs so much

YAML is harder to parse than JSON, but most of the cost is in how Helm does it. Helm reads the index with sigs.k8s.io/yaml, the Kubernetes YAML library, which works in three steps: it parses the YAML into a generic tree of maps and lists, converts that tree to JSON text, and decodes the JSON into Helm’s own types. Along the way the index is held in several shapes at once, and the generic tree, with every key and value boxed on its own, is an expensive one.

And some commands do it twice. helm pull, install, upgrade, template and show with --repo parse the index when they download it, write it to disk, and then read that copy back and parse it again. That’s why pull --repo takes twice as long as pull in the table above. It has worked that way since --repo was added in 2017, and Argo CD fetches charts from classic repositories with helm pull --repo, so it pays double.

What people do about it

  • Raise the memory limit. Everyone does this first.
  • Publish the index as JSON. Helm 3.13 added helm repo index --json (helm/helm#12245, by the same Flux maintainer). JSON is valid YAML, so every Helm can read it, and newer ones spot it and use Go’s JSON decoder instead: on Bitnami’s old “full index” archive, 0.58 s and 94 MiB instead of 2.69 s and 836 MiB. It only helps repositories that publish JSON, though, and an Artifactory user in the same thread, with a 50 MB index and 900 MB helm repo updates, says Artifactory doesn’t, as far as they know.
  • Move to OCI registries. An OCI chart has no index at all. Not everyone can move yet: the company in the Argo CD issue stays on a classic repository because it relies on repository aliases in dependencies, which Helm doesn’t support for OCI yet.
  • Cache it or stream it. Flux caches indexes in memory; a Flux maintainer saw memory drop from 2 GB to 200 MB with the cache on. Kargo has an open pull request that streams through the index to pull out one chart. Each helps only its own tool.

Two fixes

Parse it once. I’ve sent Helm a fix for the double parse, helm/helm#32839. With it, helm pull --repo goes from 2.34 s to 1.18 s on Bitnami’s index and from 5.66 s to 2.92 s on the merged one: the same as a pull from an added repository. It’s a small change, and it touches no public API.

Parse it faster. Oyl is a YAML 1.2 parser written in C. It can turn YAML straight into JSON text, without the generic tree, and Helm’s own JSON decoding takes it from there. I built Helm 4.3.1 with Oyl in that one spot, the YAML step of loading an index, and nothing else:

Helm 4.3.1 stock with Oyl
helm repo add, Bitnami’s index 1.13 s, 635 MiB 0.36 s, 272 MiB
helm pull, Bitnami’s index 1.14 s, 554 MiB 0.33 s, 194 MiB
helm repo add, merged index 2.81 s, 1,368 MiB 0.82 s, 556 MiB
helm pull, merged index 2.76 s, 1,043 MiB 0.75 s, 369 MiB

That’s three to four times faster in a third to half the memory, and Helm ends up with exactly the same index.

The catch is that Oyl is C, and Helm is built without cgo, so stock Helm can’t just switch to it; that would take a pure-Go version. For now it’s for people who build their own Helm, and some already do. Red Hat builds the helm in OpenShift GitOps with cgo, for FIPS. Argo CD’s custom tooling lets you swap in your own helm binary, and the repo server is exactly where a 900 MB helm pull hurts most.


If big indexes are costing you memory, nodes or sleep, I’d like to hear about it.

Claude found the double parse while we were testing Oyl against Helm, helped me gather the evidence for this post, and drafted it. The opinions are mine, and so are any typos.

If you’d like help putting Oyl to work, I offer that through my company, Tab Computing: tabcomputing.com/yaml.