
If you run autoscaled node groups on EKS or AKS, there is a good chance some of them are idle right now, and nobody is watching. Autoscalers are good at scaling up. They are much more cautious about scaling back down, and that caution is exactly where the extra spend hides.
This post walks through why idle capacity builds up quietly, how to actually measure it with tools you already have, and the specific Cluster Autoscaler and Karpenter settings that fix it.
Why node groups don't scale back down on their own
Cluster Autoscaler and Karpenter both add nodes fast, because an unschedulable pod is a visible, urgent problem. Removing a node is treated far more conservatively, because a bad scale-down can take a workload down with it. Several common things quietly block scale-down:
- PodDisruptionBudgets (PDBs) that leave no room for even one pod to be evicted.
- Pods with the annotation
cluster-autoscaler.kubernetes.io/safe-to-evict: "false", often left over from a one-time workaround nobody removed. - Pods using local storage (
emptyDirwith data, or hostPath) that the autoscaler won't evict by default. - System or kube-system pods with no eviction tolerance, pinning a node that otherwise has nothing useful running on it.
- Requests set far above real usage ("just in case" sizing), so a node looks busy on paper while sitting mostly empty in practice.
None of this shows up as an error. The cluster just runs a little bigger than it needs to, month after month, until someone finally looks at the bill.
Finding idle capacity with what you already have
You don't need a cost tool to find this — kubectl already tells you. Every node's "Allocated resources" block shows requests and limits as a percentage of what's allocatable:
for node in $(kubectl get nodes -o name); do
echo "== $node =="
kubectl describe "$node" | grep -A 5 "Allocated resources"
doneA node reporting something like this is a candidate for consolidation:
== node/ip-10-0-14-203.ec2.internal ==
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 420m (10%) 800m (20%)
memory 900Mi (12%) 1800Mi (24%)A single node sitting at 10% CPU requests is one pod away from being emptied out entirely — if something is stopping that from happening, it's worth finding out what.
To tie idle nodes to an actual dollar figure instead of a percentage, pull the node group's cost directly. EKS managed node groups are auto-tagged with eks:nodegroup-name, so you can filter Cost Explorer by it:
aws ce get-cost-and-usage \
--time-period Start=2026-09-01,End=2026-10-01 \
--granularity MONTHLY \
--metrics "UnblendedCost" \
--filter '{
"Tags": {
"Key": "eks:nodegroup-name",
"Values": ["ondemand-general-purpose"]
}
}'On AKS, the equivalent is filtering Cost Management by the node pool's resource group tag, or running az consumption usage list scoped to the node pool's resource group. Either way, the goal is the same: turn "this node is at 10% utilization" into "this node group cost $X last month and could run on half as many nodes."
Fixing it: tune the autoscaler before you touch workloads
The fastest wins are usually in autoscaler configuration, not application changes. If you're on Cluster Autoscaler, three flags do most of the work:
--scale-down-enabled=true
--scale-down-utilization-threshold=0.5
--scale-down-unneeded-time=10m
--scale-down-delay-after-add=10mscale-down-utilization-threshold is the one that matters most — it's the requested-resource fraction below which a node is considered for removal. The default is conservative on purpose; most teams never revisit it after setup.
On AKS, the same tuning is exposed through az aks update without touching the autoscaler directly:
az aks update \
--resource-group my-rg \
--name my-cluster \
--cluster-autoscaler-profile \
scale-down-utilization-threshold=0.5,scale-down-unneeded-time=10mIf you're already on Karpenter, consolidation is a first-class setting on the NodePool rather than a flag on the controller:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general-purpose
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
limits:
cpu: "1000"
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: defaultconsolidationPolicy: WhenEmptyOrUnderutilized tells Karpenter to actively repack underutilized nodes, not just remove ones that happen to go empty — that's the behavior most teams actually want and don't get by default.
Before you tune anything, audit the blockers
Tuning thresholds won't help if something is still forcing a node to stay. Check for these first:
- Any pod carrying
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"— confirm it's still needed, not just inherited from an old manifest. - PDBs with
minAvailableset to the full replica count, which makes every pod un-evictable by definition. - Requests that no longer match real usage — compare
kubectl top podsagainst what's requested, and right-size before blaming the autoscaler.
The checklist
- Run the node allocation check above across every node group, not just the biggest one.
- Tie at least one idle node group to a real cost figure before proposing a change — it's a much easier conversation with a dollar number attached.
- Check for
safe-to-evict: falseannotations and restrictive PDBs before touching autoscaler thresholds. - Tune
scale-down-utilization-threshold(Cluster Autoscaler) orconsolidationPolicy(Karpenter) deliberately, not by leaving the default in place forever. - Re-run the audit a month later — idle capacity creeps back as workloads change.
None of this requires a new tool or a vendor contract. It's a twenty-minute audit most clusters have never had.

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


