Skip to main content
Redhat Developers  Logo
  • AI

    Get started with AI

    • Red Hat AI
      Accelerate the development and deployment of enterprise AI solutions.
    • AI learning hub
      Explore learning materials and tools, organized by task.
    • AI interactive demos
      Click through scenarios with Red Hat AI, including training LLMs and more.
    • AI/ML learning paths
      Expand your OpenShift AI knowledge using these learning resources.
    • AI quickstarts
      Focused AI use cases designed for fast deployment on Red Hat AI platforms.
    • No-cost AI training
      Foundational Red Hat AI training.

    Featured resources

    • OpenShift AI learning
    • Open source AI for developers
    • AI product application development
    • Open source-powered AI/ML for hybrid cloud
    • AI and Node.js cheat sheet

    Red Hat AI Factory with NVIDIA

    • Red Hat AI Factory with NVIDIA is a co-engineered, enterprise-grade AI solution for building, deploying, and managing AI at scale across hybrid cloud environments.
    • Explore the solution
  • Learn

    Self-guided

    • Documentation
      Find answers, get step-by-step guidance, and learn how to use Red Hat products.
    • Learning paths
      Explore curated walkthroughs for common development tasks.
    • Guided learning
      Receive custom learning paths powered by our AI assistant.
    • See all learning

    Hands-on

    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.
    • Interactive labs
      Learn by doing in these hands-on, browser-based experiences.
    • Interactive demos
      Click through product features in these guided tours.

    Browse by topic

    • AI/ML
    • Automation
    • Java
    • Kubernetes
    • Linux
    • See all topics

    Training & certifications

    • Courses and exams
    • Certifications
    • Skills assessments
    • Red Hat Academy
    • Learning subscription
    • Explore training
  • Build

    Get started

    • Red Hat build of Podman Desktop
      A downloadable, local development hub to experiment with our products and builds.
    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.

    Download products

    • Access product downloads to start building and testing right away.
    • Red Hat Enterprise Linux
    • Red Hat AI
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Featured

    • Red Hat build of OpenJDK
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat Developer Toolset

    References

    • E-books
    • Documentation
    • Cheat sheets
    • Architecture center
  • Community

    Get involved

    • Events
    • Live AI events
    • Red Hat Summit
    • Red Hat Accelerators
    • Community discussions

    Follow along

    • Articles & blogs
    • Developer newsletter
    • Videos
    • Github

    Get help

    • Customer service
    • Customer support
    • Regional contacts
    • Find a partner

    Join the Red Hat Developer program

    • Download Red Hat products and project builds, access support documentation, learning content, and more.
    • Explore the benefits

Red Hat OpenShift autoscaling with Cluster Autoscaler

Scaling Red Hat OpenShift Compute Infrastructure with Cluster Autoscaler

August 25, 2026
Ramon Gordillo Gutierrez Jose Ortiz Padilla
Related topics:
Containers
Related products:
Red Hat OpenShift Container Platform

    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 maxReplicas limit.
    • Global ClusterAutoscaler limits (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:

    1. 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%).
    2. All pods can be rescheduled elsewhere by the Kubernetes scheduler.
    3. 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-system pods 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 marked safe-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 random
    • LeastWaste: Minimizes idle CPU (then memory) after scaling
    • Priority: 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 maxNodesTotal large enough to cover control plane machines plus all possible compute machines across every MachineAutoscaler.

    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       1

    Raw 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   15216988Ki

    Converted 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:

    MachineSetminReplicasmaxReplicasRationale
    demo-p4p95-worker-eastus1——Fixed at 1 (no MachineAutoscaler)
    demo-p4p95-worker-eastus2——Fixed at 1 (no MachineAutoscaler)
    demo-p4p95-worker-eastus313Elastic 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:
        - Random

    Apply it with kubectl:

    kubectl apply -f cluster-autoscaler.yaml

    Step 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-eastus3

    Apply it with kubectl:

    kubectl apply -f machine-autoscaler-worker-eastus3.yaml

    Step 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" \
      --overwrite

    Without 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: NoSchedule

    Apply it with kubectl:

    kubectl patch machineset demo-p4p95-worker-eastus3 -n openshift-machine-api \
    --type merge --patch-file samples/machineset-worker-eastus3-taints-patch.yaml

    Taints 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" \
    --overwrite

    Without 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-api

    Output 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     3s

    The 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-demo

    Pods 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: 512Mi

    Apply 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-test

    The 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 3

    2. 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       1

    After (3 eastus3 workers, max reached):

    NAME                        DESIRED   CURRENT   READY   AVAILABLE
    demo-p4p95-worker-eastus3   3         3         3       3

    Machines 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   3

    4. 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   4m

    All 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-ntmd9

    5. 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: 3

    All 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.yaml

    After 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 (Azure Standard_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 node

    Mitigation: 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: 512Mi

    Increasing 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.

    Related Posts

    • Autoscaling Kafka workloads with custom metrics autoscaler

    • Gang autoscaling on OpenShift with Kueue and ProvisionRequest

    • How to set up KServe autoscaling for vLLM with KEDA

    Recent Posts

    • GRPO fine-tuning on Red Hat OpenShift AI: Reinforcement learning from verifiable rewards with Training Hub

    • Red Hat OpenShift autoscaling with Cluster Autoscaler

    • Run LoRA fine-tuning on Red Hat OpenShift AI with Ray

    • Improved failure reports on Red Hat OpenShift with the event-driven diagnostic operator

    • Red Hat Ansible development workspaces for governed automation content creation

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Platforms

    • Red Hat AI
    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Build

    • Developer Sandbox
    • Developer tools
    • Interactive tutorials
    • API catalog

    Quicklinks

    • Learning resources
    • E-books
    • Cheat sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site status dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2026 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility
    Ask AI