# Prewarm node capacity with Cluster Autoscaler

Cluster Autoscaler provisions nodes when pods are pending, which means the first pod of a scale-up wave still waits for an instance to boot. Capacity buffers (`CapacityBuffer`, `autoscaling.x-k8s.io/v1beta1`) close that gap: a buffer describes spare capacity as a number of chunks of a pod shape, and the autoscaler treats those chunks as pending demand, holding warm nodes for them at all times so real pods land on warm capacity instantly.

The buffer API has one practical limitation: `spec.replicas` exists, but the CRD exposes no `/scale` subresource. The HPA, KEDA, and `kubectl scale` cannot target it, so out of the box the buffer size is a static number you edit by hand.

[Scale Adapter](https://docs.kedify.io/features/scale-adapter/) removes that limitation. Its [replica field-path mode](https://docs.kedify.io/features/scale-adapter/#targets-without-a-scale-subresource) bridges the missing `/scale` contract, so any KEDA or Kedify trigger can drive the buffer size: a cron schedule for known peaks, a queue depth or request rate for demand-driven warm pools, or the [Predictive Scaler](https://docs.kedify.io/how-to/predictive-scaler-installation/) for forecasted load.

[![KEDA drives CapacityBuffer replicas through ScaleAdapter; buffer chunks request capacity rather than running pods or a fixed number of nodes.](https://docs.kedify.io/assets/images/docs/node-capacity-buffer.svg)](https://docs.kedify.io/assets/images/docs/node-capacity-buffer.svg)

Scroll to exploreDiagram description

A KEDA ScaledObject drives a ScaleAdapter. The adapter writes CapacityBuffer replicas, representing chunks of a pod resource shape. The provider evaluates placement and provisions missing node capacity. Real application pods can then use suitable Ready nodes. Buffer chunks are capacity requests, not running application pods, and chunk count is not node count.

Cluster Autoscaler is the reference implementation of the capacity buffer [sig-autoscaling API](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/proposals/buffers.md): the proposal and the CRD live in its repository. This guide covers Cluster Autoscaler; see the [Karpenter](https://docs.kedify.io/how-to/karpenter-capacity-buffers/) and [GKE](https://docs.kedify.io/how-to/gke-capacity-buffers/) guides for the same setup on those autoscalers.

## Prerequisites

- **Cluster Autoscaler 1.34 or newer.** Buffer support is alpha in 1.34 and beta since 1.35.

- **The `CapacityBuffer` CRD**, which ships in the [cluster-autoscaler repository](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/apis/config/crd/autoscaling.x-k8s.io_capacitybuffers.yaml) and has to be applied to the cluster:

   

  ```bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/apis/config/crd/autoscaling.x-k8s.io_capacitybuffers.yaml
```

- **Two opt-in flags** on the cluster-autoscaler deployment, both off by default: `--capacity-buffer-controller-enabled` runs the buffer controller, and `--capacity-buffer-pod-injection-enabled` injects the buffers’ virtual pods into the scale-up simulation.

- **Kedify Agent v0.6.8 or newer** with the Scale Adapter controller enabled and RBAC for buffers. Field-path targets are read through an informer cache and updated as whole resources, so the grant covers the full resource:

   

  ```yaml
agent:
  features:
    scaleAdaptersEnabled: true
  extraRbacRules:
    - apiGroups: ["autoscaling.x-k8s.io"]
      resources: ["capacitybuffers"]
      verbs: ["get", "list", "watch", "update"]
```

## Create the Buffer

A buffer references a `PodTemplate` describing one chunk of capacity and a `replicas` count of how many such chunks to keep warm. Chunks are bin-packed like real pods, so N chunks does not mean N nodes; the autoscaler provisions new nodes only when the chunks no longer fit the existing capacity.

```yaml
apiVersion: v1
kind: PodTemplate
metadata:
  name: standard-workload-shape
  namespace: default
template:
  metadata:
    labels:
      app: warm-pool
  spec:
    containers:
      - name: placeholder
        image: registry.k8s.io/pause:3.9
        resources:
          requests:
            cpu: "1"
            memory: 1Gi
---
apiVersion: autoscaling.x-k8s.io/v1beta1
kind: CapacityBuffer
metadata:
  name: warm-pool
  namespace: default
spec:
  podTemplateRef:
    name: standard-workload-shape
  replicas: 1
```

The template’s containers never run. The autoscaler turns the buffer into in-memory virtual pods that participate in scheduling simulation only, so the image is never pulled and nothing is written to the cluster beyond the buffer itself.

A buffer can alternatively track a workload with `scalableRef` and `percentage`, without any `spec.replicas` field. That form cannot be adapted: the adapter needs a concrete field to write, and reports `TargetResolved=False` with reason `ReplicaPathUnreadable` if `spec.replicas` does not exist. Use `podTemplateRef` with `replicas` for buffers managed by KEDA. The optional `spec.limits` cap keeps working and bounds the buffer regardless of what the autoscaler requests.

## Adapt and Scale

Create the adapter in the buffer’s namespace, pointing the field paths at the buffer’s replica fields. Leave the adapter’s `spec.replicas` unset; the controller initializes it from the buffer, so creating the adapter never changes the buffer size:

```yaml
apiVersion: autoscaling.kedify.io/v1alpha1
kind: ScaleAdapter
metadata:
  name: warm-pool
  namespace: default
spec:
  targetRef:
    apiVersion: autoscaling.x-k8s.io/v1beta1
    kind: CapacityBuffer
    name: warm-pool
  selector:
    matchLabels:
      app: warm-pool
  desiredReplicasPath: ".spec.replicas"
  currentReplicasPath: ".status.replicas"
```

The selector is nominal: buffer chunks are virtual and never exist as Pods, so nothing will ever match it. It only satisfies the HPA’s requirement for a non-empty scale selector, see the [selector caveat](https://docs.kedify.io/features/scale-adapter/#selector-caveat).

Then point a regular `ScaledObject` at the adapter. A cron trigger that pre-provisions ten chunks of capacity ahead of the morning ramp-up and releases them at night looks like this:

```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: warm-pool
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: autoscaling.kedify.io/v1alpha1
    kind: ScaleAdapter
    name: warm-pool
  minReplicaCount: 0
  maxReplicaCount: 20
  triggers:
    - type: cron
      metadata:
        timezone: Europe/Prague
        start: 30 7 * * 1-5
        end: 0 20 * * 1-5
        desiredReplicas: "10"
```

With `minReplicaCount: 0` the buffer drops to zero chunks outside the window, and the autoscaler removes the now-empty nodes. Any other trigger works the same way, including combining several triggers so the buffer follows whichever demand signal is highest.

## Verify

```bash
kubectl get scaleadapter,capacitybuffer
kubectl get events --field-selector involvedObject.kind=CapacityBuffer
kubectl get nodes
```

Inside the cron window the adapter forwards the desired count to the buffer, the buffer emits scale-up events, and the warm nodes appear in the node list:

```text
NAME                                           TARGET      KIND             DESIRED   CURRENT   READY
scaleadapter.autoscaling.kedify.io/warm-pool   warm-pool   CapacityBuffer   10        10        True

LAST SEEN   TYPE     REASON             OBJECT                     MESSAGE
2m          Normal   TriggeredScaleUp   capacitybuffer/warm-pool   capacity buffer 2 fake pods triggered scale-up: ...
```

The buffer’s `Provisioning` condition reports `FitsExistingCapacity` when the chunks fit the cluster’s spare room without any new node; headroom you already have is free. When the trigger deactivates, the buffer reports `0` replicas and the empty nodes are removed.

## Behavior Notes

- **Reaction time.** The buffer controller re-resolves templates and counts on a polling loop of roughly 30 seconds, on top of the usual KEDA and HPA intervals. Expect up to a minute between a trigger change and the node request. That envelope is fine for the use case, since the buffer exists to absorb instance boot time on behalf of the real workload.

- **Scale-down pace.** After the buffer drops to zero, the emptied nodes are removed by the regular scale-down evaluation, governed by flags like `--scale-down-unneeded-time` (10 minutes by default). Budget for that tail when estimating the cost of short buffer windows.

- **Single writer.** The adapter is authoritative over `spec.replicas`. Do not edit the buffer size by hand or from another controller while the adapter manages it; the adapter overwrites external changes and raises the `ConflictingReplicaWriter` condition. Note that the autoscaler reacts to buffer writes within seconds, so even a short-lived external write can trigger real node provisioning before the adapter’s overwrite lands.

- **Bound the blast radius.** The `ScaledObject`’s `maxReplicaCount` caps what KEDA requests, the buffer’s `spec.limits` caps the buffer regardless of the writer, and `--max-nodes-total` with the per-node-group size limits caps the nodes. Buffers also integrate with resource quotas since 1.35; chunks blocked by quota surface through the `LimitedByQuotas` condition.

- **Reactivation and stabilization.** After a scale to zero, the HPA’s downscale stabilization window can briefly restore the last recommendation from before the idle period. If the overshoot matters, tune `spec.advanced.horizontalPodAutoscalerConfig.behavior` on the `ScaledObject`.

## Continue with this topic

**Reference:** [ScaleAdapter API reference](https://docs.kedify.io/reference/kubernetes-api/scaleadapter/).

**Diagnose:** [Workload does not scale, or scales too slowly](https://docs.kedify.io/troubleshooting/workload-scaling/).

**Related capabilities:** [Scale custom targets and prewarm node capacity](https://docs.kedify.io/capabilities/node-capacity/).

---
Canonical: https://docs.kedify.io/how-to/cluster-autoscaler-capacity-buffers/
Source: src/content/docs/how-to/cluster-autoscaler-capacity-buffers.md
Documentation index: https://docs.kedify.io/llms.txt
