For years, the way you kept spare capacity in a Kubernetes cluster was to lie to it. You deployed a pile of pods that did nothing, gave them a negative priority class so anything real could evict them, and sized the pile by hand until the start latency looked acceptable. Everyone called them balloon pods, or pause pods, or ballast. Everyone knew it was a hack. Everyone shipped it anyway, because the alternative was waiting for a node.
Karpenter 1.14 makes it a resource. That is the headline. The more useful part is the fine print, and in this case the fine print contradicts the documentation in two places.▸The 60-second version — flip through the deck8 slides · swipe →
What the hack actually did
The mechanism is worth restating, because the new object inherits its physics.
The workload never waits for EC2, because someone already paid for the node. That is the whole trick, and it is a good trick. The problems were all operational.
The pile had to be sized by hand, and re-sized whenever the workload it protected changed shape. It reported as utilization, so every dashboard overstated how busy the cluster was. And it lived entirely outside the NodePool that governed everything else about your capacity, which meant two systems with opinions about node count and no shared vocabulary.
The object
CapacityBuffer is namespaced, and you describe the buffer either by a PodTemplate or by pointing at something that already exists.
apiVersion: autoscaling.x-k8s.io/v1beta1
kind: CapacityBuffer
metadata:
name: api-headroom
namespace: default
spec:
scalableRef:
kind: Deployment
name: api
percentage: 20
limits:
cpu: "32"
That is the shape I reach for first, because it removes the sizing problem instead of moving it. The buffer is defined as a proportion of a workload that already scales, so when the workload grows the headroom grows with it and nobody has to remember.
The fields, and what each one is actually for:
| Field | Meaning |
|---|---|
podTemplateRef | Shape of one buffer chunk, from a PodTemplate in the same namespace |
scalableRef | A Deployment, StatefulSet or ReplicaSet whose pod template defines the chunk |
replicas | A fixed number of chunks |
percentage | Chunks as a percentage of scalableRef replicas, rounded up to at least 1 |
limits | A resource ceiling that caps how many chunks get created |
podTemplateRef and scalableRef are mutually exclusive, and the CRD enforces it with a validation rule. There is a second rule worth knowing before you write a template-based buffer: if you set podTemplateRef, you must also set replicas or limits, or the object is rejected.
The consolidation seam
This is the part that tells you the feature was designed rather than bolted on, and it is the first question any Karpenter operator should ask. Buffer pods are virtual. They are not real pods. So what stops Karpenter from noticing an empty node and consolidating away the exact capacity you asked it to reserve?
Without the first rule the feature would eat itself: reserve capacity, Karpenter sees nodes with no real pods, Karpenter deletes them. With it, the buffer survives while the rest of the disruption machinery keeps working, which is the correct trade.
Where the docs and the CRD disagree
Now the part worth checking before you write any YAML.
The concept page on karpenter.sh documents the API as autoscaling.x-k8s.io/v1alpha1. The CRD in kubernetes-sigs/karpenter serves exactly one version, and it is not that one:
$ kubectl get crd capacitybuffers.autoscaling.x-k8s.io \
-o jsonpath='{.spec.versions[*].name}'
v1beta1
The upstream bump to v1beta1 merged on 9 July 2026, two days before the provider's 1.14.0 release. The docs page has not followed. Copy the documented manifest onto a 1.14 cluster and you get no matches for kind "CapacityBuffer" in version "autoscaling.x-k8s.io/v1alpha1", which is a confusing error to receive from a feature you just installed.
The second disagreement is subtler and matters more, because it changes the size of your bill.
min(max(replicas, percentage), limits). The larger of the two, then capped.docsSet replicas: 10 and percentage: 20 against a 100-replica Deployment and one reading gives you 20 chunks of headroom while the other gives you 10. That is a factor of two on capacity you are paying for and not using.
I have not run the controller against both readings to see which one wins in practice, so I am not going to tell you which is right. I am going to tell you to check on your own cluster before you size anything:
kubectl explain capacitybuffer.spec.replicas
kubectl explain capacitybuffer.spec.percentage
kubectl explain reads the schema the API server is actually serving. It cannot be out of date the way a website can.
The cluster ships the contract. The docs only describe it.
The rest of the second half
Three more things live in the caveats, and all three are the kind you discover at the worst time.
- PVC-backed and ephemeral volumes in the pod template are stripped from the virtual pods. A buffer for a workload whose scheduling depends on volume topology is not reserving what you think it is.
- The buffer controller requeues every 30 seconds, so a replica change on the tracked Deployment takes up to half a minute to reach the buffer. Fine for capacity planning, not fine as a reaction to a traffic spike.
- Buffer pods are still subject to NodePool limits. Exhaust the CPU or memory ceiling on the NodePool and the buffer simply is not fulfilled, quietly, because there is nothing to fail.
That last one deserves an alert rather than a comment in a values file. A buffer that silently is not there is worse than no buffer, because the whole point was that you stopped thinking about start latency.
Is it worth it
The honest framing has not changed since the balloon-pod era, and the new object does not soften it.
Reserved headroom is not a safety feature you enable. It is nodes you rent and deliberately leave empty, so that the next pod does not wait for EC2. The bill arrives whether the spike comes or not. What Karpenter 1.14 changes is that the decision is now written down in the same place as the rest of your capacity policy, sized against a real workload instead of a guess, and visible to the controller that manages your nodes rather than hidden in a Deployment nobody remembers owning.
That is a real improvement, and it is one less piece of infrastructure to babysit. It is not a discount.
A buffer is not insurance. It is a decision to pay for empty nodes in exchange for start time. Make it on purpose.
Running balloon pods today? I would genuinely like to know what number you landed on and how you picked it, because that figure is usually inherited rather than chosen. Come argue with me on LinkedIn.