EKS node groups running more nodes than their load needs, trimmed one at a time
What does ZopNight detect here?
A node group averaging 40% CPU or less with desired above its minimum can absorb one drained node's pods across the survivors, so ZopNight recommends shrinking DesiredSize by exactly 1 (priced at that node's on-demand rate × 730 hours) and re-measures before ever claiming a second.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-042 |
| Category | rightsizing |
| Severity | medium |
| Metric | none — pure configuration read |
| Source | eks_underutilized_nodes.go |
Where it applies
One node per pass, on purpose
A 10-node group at 10% CPU could arguably lose several nodes at once. The rule still only ever claims one: under-claiming is the safe direction for an action that drains live pods, and successive passes converge on the right size with a fresh measurement between each step. The gate is desired count above the scaling-config floor plus a 14-day CPU average at or under 40%: headroom enough for the remaining nodes to absorb the drained node’s pods without tipping into pressure.
Three vetoes stand between the average and the recommendation
Memory first: a CPU-idle group holding JVM heaps or in-memory caches must not shed a node, so the memory rollup is required and must average at or under 50%. Peaks second: a group that spikes toward saturation needs its headroom at exactly those moments, so a trusted peak near the ceiling suppresses regardless of the average. Capacity type third: Spot groups abstain entirely, because only the on-demand instance rate is visible here and pricing a removed Spot node at on-demand would overstate the saving two to three times.
Evidence requirements, stated
Both rollups come from the Container Insights agent EKS documents; a cluster without the agent produces no series and the rule stays silent. A blind scale-down on an unmeasured group is the failure mode it refuses. Unhealthy or transitioning node groups are skipped, and a missing on-demand rate abstains rather than emitting an unpriced recommendation.
Check a group’s headroom
aws eks describe-nodegroup --cluster-name my-cluster --nodegroup-name my-ng \ --query 'nodegroup.[scalingConfig,capacityType,instanceTypes]'aws cloudwatch get-metric-statistics \ --namespace ContainerInsights --metric-name node_cpu_utilization \ --dimensions Name=ClusterName,Value=my-cluster Name=NodegroupName,Value=my-ng \ --start-time "$(date -u -v-14d +%Y-%m-%dT%H:%M:%SZ)" \ --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --period 86400 --statistics AverageThe PDB is your half of the contract
Applying the change sets DesiredSize down by one; AWS cordons and drains the surplus node, honouring PodDisruptionBudgets as it evicts. That guarantee is only as good as the PDBs you have defined, and a workload without one can be evicted abruptly. The pre-flight check is therefore that every workload on the group carries a PDB, and the post-flight check is a day of latency and error metrics on the smaller fleet.