There are different solutions for scaling your Red Hat OpenShift compute infrastructure, including:
- Cluster Autoscaler: The built-in, Kubernetes-native approach, integrated with the Red Hat OpenShift machine API.
- MachineSet Autoscaler with a custom autoscaler (KEDA): A metrics-driven approach that scales individual MachineSets based on external or custom signals.
This article examines Cluster Autoscaler, with an example on Microsoft Azure Red Hat OpenShift. The next article in this series covers a KEDA-based MachineSet Autoscaler driven by Prometheus CPU utilization, and a third article compares both approaches.
Cluster Autoscaler in Red Hat OpenShift
Cluster Autoscaler automatically adjusts the size of an Red Hat OpenShift Container Platform cluster to meet current workload demands. It is based on the upstream Cluster Autoscaler project and is integrated with OpenShift through the ClusterAutoscalerOperator, which exposes 2 custom resources:
ClusterAutoscaler
- API:
autoscaling.openshift.io/v1 - Scope: Cluster-wide
- Role: Decides when and how much to scale, enforces global limits
MachineAutoscaler
- API:
autoscaling.openshift.io/v1beta1 - Scope: Per MachineSet
- Role: Defines which MachineSets can scale and their minimum and maximum bounds.
Both resources are required. The Cluster Autoscaler alone cannot scale anything. It relies on MachineAutoscaler resources to annotate eligible MachineSets and to define per–MachineSet replica boundaries. If you deploy a ClusterAutoscaler without any MachineAutoscaler objects, the cluster never scales.
How it works
Cluster Autoscaler is only available on clusters where the Machine API operator is operational (installer-provisioned infrastructure on supported cloud providers).
Scale-up trigger
By default, Cluster Autoscaler increases cluster size when:
- Pods remain Pending because no worker node has sufficient CPU, memory, or other schedulable resources.
- A pod cannot be scheduled due to constraints (node selectors, affinity, taints) that require an additional node of a specific type.
That pending-pod path is the classic, always-on behavior demonstrated in this article. Cluster Autoscaler also supports predictive and proactive scale-up using 2 APIs:
- ProvisioningRequest: Request capacity for a group of pods before (or as part of) admitting the workload (for example, gang provisioning with Kueue).
- CapacityBuffer: Declare spare capacity that the autoscaler treats as demand, so nodes can be pre-provisioned ahead of real workloads.
In the pending-pod path, the autoscaler only adds nodes when doing so would result in a schedulable pod. It does not scale up if:
- Available MachineSet node types cannot satisfy the pod's resource or constraint requirements.
- All eligible MachineSets are already at their
maxReplicaslimit. - Global
ClusterAutoscalerlimits (cores, memory, GPUs,maxNodesTotal) would be exceeded.
Scale-down behavior
Every 10 seconds, the Cluster Autoscaler evaluates whether any worker nodes are unnecessary and can be removed. A node is eligible for removal when:
- Utilization is below the threshold: Node utilization level (sum of requested resources divided by node allocatable resources) is less than the configured utilization threshold (default is
0.5, which is 50%). - All pods can be rescheduled elsewhere by the Kubernetes scheduler.
- The node does not have the scale-down disabled annotation.
The autoscaler does not remove a node that hosts:
- Pods with restrictive PodDisruptionBudgets (PDBs).
kube-systempods that are not normally present on worker nodes, or that lack an appropriate PDB.- Pods not owned by a controller (Deployment, ReplicaSet, StatefulSet, and so on).
- Pods with local storage.
- Pods that cannot be moved (insufficient cluster capacity, anti-affinity conflicts, incompatible selectors).
- Pods annotated with
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"(unless also markedsafe-to-evict: "true").
Resource accounting
To align with the cluster scheduling logic, the Cluster Autoscaler relies on allocatable capacity (status.allocatable) rather than total raw node resources when checking pod scheduling feasibility or verifying global ClusterAutoscaler resourceLimits (cores.min/max, memory.min/max). By doing so, it successfully includes system constraints and kubelet reservations in scale-up decisions and threshold validations.
Resource metrics (CPU, memory, and GPU) are collected from all cluster elements, including control plane nodes, even though the autoscaler does not control those specific MachineSets. Global limits are checked against this cumulative, cluster-wide allocatable pool instead of being restricted to single MachineSets.
When choosing which MachineSet to expand, the autoscaler uses an expander strategy:
Random: By default, selects a MachineSet at randomLeastWaste: Minimizes idle CPU (then memory) after scalingPriority: Uses a ConfigMap with regex patterns to prefer higher-priority MachineSets
Limitations
When using Cluster Autoscaler, keep these constraints in mind:
- Avoid making manual changes to individual nodes within an autoscaled MachineSet, because system pods, labels, and total capacity are shared equally across all nodes in the group.
- Always specify resource requests on pods; the autoscaler uses requests (not limits) for scheduling simulation.
- Configure PDBs for workloads that must not be evicted during scale-down.
- Ensure cloud provider quotas support the maximum node counts configured.
- Do not run additional cloud-provider node group autoscalers alongside Cluster Autoscaler.
- Set
maxNodesTotallarge enough to cover control plane machines plus all possible compute machines across everyMachineAutoscaler.
Example: Microsoft Azure Red Hat OpenShift cluster (scaling eastus3 from 1 to 3)
The following example was built and verified on an OpenShift 4.20 cluster deployed with installer-provisioned infrastructure on Microsoft Azure.
Cluster inventory
Microsoft Azure virtual machine sizes and allocatable resources (status.allocatable) reported on demo-p4p95:
Control plane
- Azure VM size:
Standard_D8s_v3 - Count: 3 (fixed)
- CPU: 7500m
- Memory: 30.2 GiB
Worker
- Azure VM size:
Standard_D4s_v3 - Count: 3 → 5 (2 fixed + 1–3 eastus3)
- CPU: 3500m
- Memory: 14.5 GiB
ClusterAutoscaler resourceLimits calculate total available CPU and memory across every node, including the control plane. For unprovisioned nodes, the Cluster Autoscaler relies on capacity annotations from the MachineAutoscaler operator.
Worker MachineSets at install time:
NAME DESIRED CURRENT READY AVAILABLEdemo-p4p95-worker-eastus1 1 1 1 1demo-p4p95-worker-eastus2 1 1 1 1demo-p4p95-worker-eastus3 1 1 1 1Raw allocatable values from the cluster:
$ kubectl get nodes -o \
custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory
NAME CPU MEMORY
demo-p4p95-master-0 7500m 31706428Ki
demo-p4p95-worker-eastus3-xpnp4 3500m 15216988KiConverted to GiB (Ki ÷ 1024²), each master is about 30.2 GiB, and each worker is about 14.5 GiB.
MachineAutoscaler capacity annotations on demo-p4p95-worker-eastus3 (used to simulate hypothetical nodes during scale-up decisions):
{
"capacity.cluster-autoscaler.kubernetes.io/cpu": "4",
"capacity.cluster-autoscaler.kubernetes.io/memory": "17179869184",
"capacity.cluster-autoscaler.kubernetes.io/labels": "kubernetes.io/arch=amd64,topology.kubernetes.io/zone=eastus-3",
"capacity.cluster-autoscaler.kubernetes.io/taints": "machineset-autoscaler/demo=eastus3:NoSchedule"
}
Nodes in this MachineSet carry taint machineset-autoscaler/demo=eastus3:NoSchedule so only the scale-test workload schedules there (Step 4).
Sizing the limits
The cluster baseline has 6 nodes (3 masters + 2 fixed workers + 1 eastus3 worker). Scaling eastus3 from 1 to 3 adds up to 2 more nodes. For hypothetical nodes (not yet provisioned), the CA uses capacity annotations (raw vCPU: 4 per worker) rather than allocatable (3.5):
maxNodesTotal
- Calculation: 3 masters + 2 fixed + 3 eastus3
- Value: 8
cores.min
- Calculation: (3 × 7.5) + (3 × 3.5)
- Value: 33
cores.max
- Calculation: 33 + (2 × 4) ← capacity annotation
- Value: 44
memory.min
- Calculation: (3 × 30.2) + (3 × 14.5) GiB
- Value: 134 GiB
memory.max
- Calculation: 134 + (2 × 16) GiB ← capacity annotation
- Value: 170 GiB
In the custom resource (CR), cores values are whole numbers, so 7500m equals 7.5 cores and 3500m equals 3.5 cores. memory values are measured in GiB. For hypothetical nodes, the CA uses the capacity.cluster-autoscaler.kubernetes.io/cpu annotation (raw 4 vCPU) rather than actual allocatable.
Only demo-p4p95-worker-eastus3 gets a MachineAutoscaler. The other 2 MachineSets remain at their fixed replica counts:
| MachineSet | minReplicas | maxReplicas | Rationale |
|---|---|---|---|
| demo-p4p95-worker-eastus1 | — | — | Fixed at 1 (no MachineAutoscaler) |
| demo-p4p95-worker-eastus2 | — | — | Fixed at 1 (no MachineAutoscaler) |
| demo-p4p95-worker-eastus3 | 1 | 3 | Elastic target: scales 1 → 3 (zone 3) |
Step 1: Deploy the ClusterAutoscaler
Here is the cluster-autoscaler.yaml:
apiVersion: autoscaling.openshift.io/v1
kind: ClusterAutoscaler
metadata:
name: default
spec:
resourceLimits:
maxNodesTotal: 8
cores:
min: 33
max: 44
memory:
min: 134
max: 170
scaleDown:
enabled: true
delayAfterAdd: 5m
delayAfterDelete: 2m
delayAfterFailure: 30s
unneededTime: 3m
utilizationThreshold: "0.5"
scaleUp:
newPodScaleUpDelay: 10s
expanders:
- RandomApply it with kubectl:
kubectl apply -f cluster-autoscaler.yamlStep 2: Deploy MachineAutoscaler for eastus3
The machine-autoscaler-worker-eastus3.yaml allows zone 3 to scale from 1 to 3 nodes:
apiVersion: autoscaling.openshift.io/v1beta1
kind: MachineAutoscaler
metadata:
name: demo-p4p95-worker-eastus3
namespace: openshift-machine-api
spec:
minReplicas: 1
maxReplicas: 3
scaleTargetRef:
apiVersion: machine.openshift.io/v1beta1
kind: MachineSet
name: demo-p4p95-worker-eastus3Apply it with kubectl:
kubectl apply -f machine-autoscaler-worker-eastus3.yamlStep 3: Add capacity label annotation (required for zone affinity on hypothetical nodes)
The Cluster Autoscaler evaluates adding a new node to satisfy zone-affinity pods, but it must know what labels that hypothetical node would carry. The MachineAutoscaler operator sets capacity.cluster-autoscaler.kubernetes.io/cpu and /memory automatically, but the zone label must be added explicitly:
kubectl annotate machineset demo-p4p95-worker-eastus3 -n openshift-machine-api \
"capacity.cluster-autoscaler.kubernetes.io/labels=kubernetes.io/arch=amd64,topology.kubernetes.io/zone=eastus-3" \
--overwriteWithout this annotation, Cluster Autoscaler logs No expansion options because it cannot determine that the new node would satisfy the pod's zone affinity constraint.
Step 4: Taint eastus3 workers (dedicated pool for scale tests)
Add a NoSchedule taint on the MachineSet so only workloads with a matching toleration can land on eastus3 nodes. System daemons (with their own tolerations) still run; general application pods stay off this pool. Here's the machineset-worker-eastus3-taints-patch.yaml file:
spec:
template:
spec:
taints:
- key: machineset-autoscaler/demo
value: eastus3
effect: NoScheduleApply it with kubectl:
kubectl patch machineset demo-p4p95-worker-eastus3 -n openshift-machine-api \
--type merge --patch-file samples/machineset-worker-eastus3-taints-patch.yamlTaints on the MachineSet template apply to new machines only. Patch existing Machine objects (or run samples/machineset-worker-eastus3-apply-taints.sh) so nodes already in the cluster pick up the taint.
For Cluster Autoscaler scale-up simulation, annotate taints on hypothetical nodes:
kubectl annotate machineset demo-p4p95-worker-eastus3 -n openshift-machine-api \
"capacity.cluster-autoscaler.kubernetes.io/taints=machineset-autoscaler/demo=eastus3:NoSchedule" \
--overwriteWithout the taints annotation, Cluster Autoscaler may not expand the group for pods that tolerate the taint but have no other matching node.
Step 5: Verify deployment
Verify your deployment:
kubectl get clusterautoscaler
kubectl get machineautoscaler -n openshift-machine-apiOutput from demo-p4p95:
NAME AGE
default 2s
NAME REF KIND REF NAME MIN MAX AGE
demo-p4p95-worker-eastus3 MachineSet demo-p4p95-worker-eastus3 1 3 3sThe MachineAutoscaler operator annotates the target MachineSet immediately:
{
"machine.openshift.io/cluster-api-autoscaler-node-group-min-size": "1",
"machine.openshift.io/cluster-api-autoscaler-node-group-max-size": "3",
"capacity.cluster-autoscaler.kubernetes.io/cpu": "4",
"capacity.cluster-autoscaler.kubernetes.io/memory": "17179869184",
"capacity.cluster-autoscaler.kubernetes.io/labels": "kubernetes.io/arch=amd64,topology.kubernetes.io/zone=eastus-3",
"capacity.cluster-autoscaler.kubernetes.io/taints": "machineset-autoscaler/demo=eastus3:NoSchedule"
}Step 6: Trigger scale-up with a test workload
The scale test runs in a dedicated test namespace. The test-namespace.yaml file:
apiVersion: v1
kind: Namespace
metadata:
name: test
labels:
app.kubernetes.io/part-of: machineset-autoscaler-demoPods are restricted to zone eastus-3 and tolerate the eastus3 MachineSet taint (machineset-autoscaler/demo=eastus3:NoSchedule). The scale-test-deployment.yaml configuration deploys 9 replicas in the test namespace, each requesting 1 CPU. With 1 eastus3 worker (3.5 allocatable CPU), zone affinity, and the taint in place, 3 pods fit on the existing node and 6 remain Pending:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler-scale-test
namespace: test
labels:
app: cluster-autoscaler-scale-test
spec:
replicas: 9
selector:
matchLabels:
app: cluster-autoscaler-scale-test
template:
metadata:
labels:
app: cluster-autoscaler-scale-test
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- eastus-3
tolerations:
- key: machineset-autoscaler/demo
operator: Equal
value: eastus3
effect: NoSchedule
containers:
- name: pause
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
command: ["sleep", "infinity"]
resources:
requests:
cpu: "1"
memory: 512MiApply with kubectl:
kubectl apply -f test-namespace.yaml
kubectl apply -f scale-test-deployment.yaml
kubectl get pods -n test -l app=cluster-autoscaler-scale-testThe result:
NAME READY STATUS NODE
cluster-autoscaler-scale-test-8657c6c5f6-7gf89 1/1 Running demo-p4p95-worker-eastus3-xpnp4
cluster-autoscaler-scale-test-8657c6c5f6-8lmmd 1/1 Running demo-p4p95-worker-eastus3-xpnp4
cluster-autoscaler-scale-test-8657c6c5f6-9vj4g 1/1 Running demo-p4p95-worker-eastus3-xpnp4
cluster-autoscaler-scale-test-8657c6c5f6-hqd7f 0/1 Pending <none>
cluster-autoscaler-scale-test-8657c6c5f6-hr88x 0/1 Pending <none>
cluster-autoscaler-scale-test-8657c6c5f6-jwd7q 0/1 Pending <none>
cluster-autoscaler-scale-test-8657c6c5f6-pbnj2 0/1 Pending <none>
cluster-autoscaler-scale-test-8657c6c5f6-sk276 0/1 Pending <none>
cluster-autoscaler-scale-test-8657c6c5f6-tvcn4 0/1 Pending <none>Scheduler events on pending pods confirm insufficient capacity on the existing node:
Warning FailedScheduling default-scheduler
0/6 nodes are available: 1 Insufficient cpu, 1 node(s) had untolerated taint {machineset-autoscaler/demo: eastus3},
1 node(s) didn't match Pod's node affinity/selector, 3 node(s) had untolerated taint {node-role.kubernetes.io/master: }.Verification: Cluster Autoscaler working
Follow these steps to verify that autoscaling is working as expected.
1. Cluster Autoscaler detected unschedulable pods and initiated scale-up
With 1 eastus3 node already running, 3 pods scheduled immediately and 6 remained Pending. Cluster Autoscaler detected the unschedulable pods and scaled up:
I0701 22:15:42.437401 orchestrator.go:185] Best option to resize: MachineSet/openshift-machine-api/demo-p4p95-worker-eastus3
I0701 22:15:42.437429 orchestrator.go:189] Estimated 2 nodes needed in MachineSet/openshift-machine-api/demo-p4p95-worker-eastus3
I0701 22:15:42.437477 orchestrator.go:261] Final scale-up plan: [{MachineSet/openshift-machine-api/demo-p4p95-worker-eastus3 1->3 (max: 3)}]
I0701 22:15:42.437500 executor.go:164] Scale-up: setting group MachineSet/openshift-machine-api/demo-p4p95-worker-eastus3 size to 32. Kubernetes events recorded the scale-up decision
Proof that Kubernetes events have recorded the scale-up decision:
LAST SEEN TYPE REASON MESSAGE
8m Normal ScaledUpGroup Scale-up: setting group MachineSet/openshift-machine-api/demo-p4p95-worker-eastus3 size to 3 instead of 1 (max: 3)3. MachineSet replica count increased, and Azure provisioned 2 additional VMs
Before (1 eastus3 worker):
NAME DESIRED CURRENT READY AVAILABLE
demo-p4p95-worker-eastus3 1 1 1 1After (3 eastus3 workers, max reached):
NAME DESIRED CURRENT READY AVAILABLE
demo-p4p95-worker-eastus3 3 3 3 3Machines in Azure zone 3:
NAME PHASE TYPE REGION ZONE
demo-p4p95-worker-eastus3-xpnp4 Running Standard_D4s_v3 eastus 3
demo-p4p95-worker-eastus3-tvgkf Running Standard_D4s_v3 eastus 3
demo-p4p95-worker-eastus3-ntmd9 Running Standard_D4s_v3 eastus 34. New worker nodes joined the cluster
Verify that new worker nodes have joined the cluster:
NAME STATUS ROLES AGE
demo-p4p95-worker-eastus3-xpnp4 Ready worker 63m (existing)
demo-p4p95-worker-eastus3-tvgkf Ready worker 10m
demo-p4p95-worker-eastus3-ntmd9 Ready worker 4mAll 9 pods scheduled across the 3 nodes (3 pods per node):
cluster-autoscaler-scale-test-8657c6c5f6-7gf89 Running demo-p4p95-worker-eastus3-xpnp4
cluster-autoscaler-scale-test-8657c6c5f6-8lmmd Running demo-p4p95-worker-eastus3-xpnp4
cluster-autoscaler-scale-test-8657c6c5f6-9vj4g Running demo-p4p95-worker-eastus3-xpnp4
cluster-autoscaler-scale-test-8657c6c5f6-hqd7f Running demo-p4p95-worker-eastus3-tvgkf
cluster-autoscaler-scale-test-8657c6c5f6-hr88x Running demo-p4p95-worker-eastus3-tvgkf
cluster-autoscaler-scale-test-8657c6c5f6-jwd7q Running demo-p4p95-worker-eastus3-tvgkf
cluster-autoscaler-scale-test-8657c6c5f6-pbnj2 Running demo-p4p95-worker-eastus3-ntmd9
cluster-autoscaler-scale-test-8657c6c5f6-sk276 Running demo-p4p95-worker-eastus3-ntmd9
cluster-autoscaler-scale-test-8657c6c5f6-tvcn4 Running demo-p4p95-worker-eastus3-ntmd95. Cluster Autoscaler status reports healthy state at max capacity
From the cluster-autoscaler-status ConfigMap in openshift-machine-api:
autoscalerStatus: Running
clusterWide:
health:
status: Healthy
nodeCounts:
registered:
total: 8
ready: 8
scaleUp:
status: NoActivity
nodeGroups:
- name: MachineSet/openshift-machine-api/demo-p4p95-worker-eastus3
cloudProviderTarget: 3
minSize: 1
maxSize: 3All 9 pods Running because the MachineSet reached 3 nodes (the maximum). The timeline:
- T+0s: Deployed 9 pods (1 CPU each, zone eastus-3) — 3 Running, 6 Pending
- T+~10m: Cluster Autoscaler detected 6 unschedulable pods, scaled MachineSet 1 → 3
- T+~17m: Second node ready — 3 more pods Running (6 total)
- T+~24m: Third node ready — last 3 pods Running
- T+~24m: All 9 pods Running — scale-up complete
Cleanup
Remember to clean up ClusterAutoscaler and MachineAutoscaler resources previously created to avoid any issue when testing other alternatives.
kubectl delete -f scale-test-deployment.yamlAfter scaleDown delays elapse, eastus3 workers scale back to minReplicas (1) automatically.
Summary of Cluster Autoscaler characteristics
- Trigger: Unschedulable pods by default. Also ProvisioningRequest and CapacityBuffer for predictive capacity.
- Signal: Kubernetes scheduler simulation (plus ProvReq / buffer demand) not arbitrary external metrics.
- Scope: Cluster-wide limits, per–MachineSet bounds with MachineAutoscaler.
- Scale-down: Automatic, based on node utilization and pod eviction safety.
- Integration: Native OpenShift CRDs, with no additional operators required.
- Best for: General-purpose capacity management driven by pod scheduling pressure (with optional predictive APIs).
Limitation: Reactive scaling and node boot time
In the default path, Cluster Autoscaler is reactive. It requests a new node after a pod is already Pending and the scheduler confirms it cannot be placed on any existing node (predictive alternatives ProvisioningRequest and CapacityBuffer are covered in the next section). That decision itself is fast. On demo-p4p95, the autoscaler detected 6 unschedulable pods and increased the eastus3 MachineSet replica count within ~10 minutes of deployment, but provisioning the VM and joining it to the cluster takes much longer.
The end-to-end delay is the sum of several steps (read Cluster Autoscaler FAQ: How fast is HPA when combined with CA? for more information):
HPA reaction (if used)
- Duration: ~30 s – 3 min
- Metrics scrape interval, backoff.
Cluster Autoscaler reaction
- Duration: ~10 s (default)
- Configurable with
scaleUp.newPodScaleUpDelay.
Cloud VM provisioning
- Duration: 3 – 7+ min
- Provider-dependent. On
demo-p4p95(AzureStandard_D4s_v3) each new eastus3 worker took ~7 min from scale-up request to Ready.
Pod scheduling on new node
- Duration: ~10 – 30 s
- After the node registers and passes readiness checks.
Workload pods stay Pending during the provisioning process. For latency-sensitive or bursty workloads, this wait is often too long, even when autoscaling is configured correctly.
Pending pod detected
│
▼ ~10 s
Cluster Autoscaler increases MachineSet replicas
│
▼ 3–7+ min (cloud provider + new node preparation)
New VM provisioned, node joins cluster
│
▼ ~10–30 s
Pending pods scheduled on new nodeMitigation: Overprovisioning with pause pods
A common solution is overprovisioning: running low-priority pause pods to hold extra CPU and memory in reserve. When real workloads arrive, they displace the pause pods. The pause pods then move to a Pending state, triggering Cluster Autoscaler to add nodes before production workloads suffer delays.
This pattern is documented in the Cluster Autoscaler FAQ. The essential pieces are:
- A PriorityClass with a low value (for example,
-10) set for overprovisioning pods, live workloads can preempt them without issue. The value remains high enough to add nodes when the pause pods cannot run elsewhere. - A Deployment of pause containers (
registry.k8s.io/pause) with resource requests matching the headroom you want to keep warm. - Optionally, a Cluster Proportional Autoscaler to adjust pause-pod replicas dynamically as the cluster grows or shrinks (for example, keep one pause pod per N cores).
Minimal static example:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -10
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
namespace: default
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
priorityClassName: overprovisioning
terminationGracePeriodSeconds: 0
containers:
- name: reserve-resources
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: "1"
memory: 512MiIncreasing replicas or per-pod requests reserves more headroom and tends to keep more nodes online ahead of demand.
Side effects of overprovisioning
Overprovisioning trades cost and complexity for lower scheduling latency:
- Higher baseline cost: Nodes run idle pause containers that consume allocatable resources you pay for but do not use productively
- Artificial cluster size: The cluster may hold more nodes than current workloads require, reducing scale-down opportunities
- Priority preemption required: Real workloads must use a higher PriorityClass than pause pods (≥ 0 compared to -10) or they cannot displace the placeholders
- Operational overhead: Static sizing is simple but inflexible; dynamic sizing adds the Cluster Proportional Autoscaler and tuning of ratios
- Misconfiguration risk: Too much overprovisioning wastes money; too little leaves the same Pending delay as before
Overprovisioning does not change how Cluster Autoscaler works. It simply changes when scaling triggers by keeping placeholder pods pending before production traffic arrives.
Predictive scaling with ProvisioningRequest and CapacityBuffer
Pending pods and pause-pod overprovisioning are not the only ways to drive Cluster Autoscaler. Upstream CA (and OpenShift's integration of it) also expose APIs that request capacity before production pods become unschedulable:
ProvisioningRequest (autoscaling.x-k8s.io)
- Role: Ask Cluster Autoscaler for capacity for a group of pods (check-capacity or best-effort atomic scale-up). Often used with Kueue for gang admission: capacity is provisioned (or confirmed) before the job is admitted.
- OpenShift status (standalone): Developer Preview, enable with
featureSet: DevPreviewNoUpgrade
CapacityBuffer (autoscaling.x-k8s.io)
- Role: Declare spare capacity (fixed or proportional to a scalable target). A controller turns the buffer into virtual or fake pods that Cluster Autoscaler treats as demand, so nodes can stay warm without managing pause Deployments by hand.
- OpenShift status (standalone): Emerging upstream CA API; availability on a given OpenShift release depends on the Cluster Autoscaler build.
These APIs handle scaling decisions directly within Cluster Autoscaler's scheduler simulation. While they are not a substitute for PromQL or business metrics, they enable predictive capacity requests and offer a cleaner spare-capacity model than traditional pause pods.
For gang-style AI/ML flows on OpenShift, see Gang autoscaling on OpenShift with Kueue and ProvisioningRequest.
Why consider a different approach?
Cluster Autoscaler triggers scale-ups based on scheduling pressure, even when using overprovisioning. It does not read queue depth or business metrics like KEDA does. ProvisioningRequest and CapacityBuffer help cut down wait times for pending workloads, but metric-based MachineSet scaling remains essential when you want to scale on resource utilization, queue length, or external systems.