# AKS Node Pool Underutilized

> ZopNight flags an AKS node pool when node CPU averages under 10% across 30 days while memory stays below 50%, then prices the move to the next-smaller VM size in the same family from real hourly rates. Memory-bound pools and pools with no priced smaller SKU are never flagged.

Source: https://zop.dev/integrations/azure/recommendations/aks-node-pool-underutilized
Updated: 2026-08-19

---

## Kubernetes hides idle nodes well

Cluster dashboards show pods, deployments, and namespaces, not the fact that every node in a
pool is coasting at single-digit CPU. The bill, meanwhile, is driven entirely by the VM size
and node count underneath. ZopNight reads the platform's node-level CPU series for the agent
pool; an average under 10% across the window marks the pool as provisioned well past its
workload.

## Memory is the veto that keeps this safe

Plenty of Kubernetes workloads are memory-hungry and CPU-light, JVM services being the
classic case. A pool at 8% CPU but 70% memory is not oversized; it is memory-bound, and
smaller nodes would evict pods. The rule therefore checks the full memory series and suppresses
the finding whenever memory utilization reaches 50%. Crucially, this guard reads the entire
harvested history, not a recent slice: a pool that ran memory-hot three weeks ago keeps its
protection even if it cooled since. Disk utilization is attached as context but never gates.

## Savings priced from the actual SKU ladder

The recommendation names the next-smaller VM size in the same family, so a Standard_D8s_v5 pool
steps to Standard_D4s_v5. The dollar figure is the real hourly-rate delta between the
two SKUs in your region, never a flat percentage. Both rates must be present in the pricing
catalog, and SKUs that AKS restricts are filtered from the ladder. No nameable target or no
rate means abstain.

## Watch pool-level utilization directly

```bash
az monitor metrics list \
  --resource "$(az aks show -n <cluster> -g <rg> --query id -o tsv)" \
  --metric node_cpu_usage_percentage node_memory_working_set_percentage \
  --interval PT1H --offset 30d --aggregation Average \
  --filter "nodepool eq '<pool>'"
```

## Acting on the finding

Two paths capture the saving: create a new pool at the smaller size and drain workloads over
with node selectors, or reduce node count and let the cluster autoscaler enforce the floor.
Watch pending pods and PodDisruptionBudget violations during the move.

## What the detection needs

Reader and Monitoring Reader cover it; the CPU series must span at least 7 trustworthy days
before any decision. ZopNight's AKS write role can resize pools during scheduled windows, but
this finding ships as advice, not automation.
