intermediate 2 min answer

A platform team cuts CPU and memory requests across 900 Kubernetes deployments. Average pod requests drop 40%, the rightsizing dashboard shows the win, and three weeks later the monthly node bill is unchanged. Where did the saving go?

kubernetesbin-packingrightsizingautoscalernode-cost
Show the full answer Hide the answer

The first three things I would look at

  1. Bin-packing efficiency: the sum of pod requests divided by the sum of node allocatable capacity, per node pool. This is the only number that connects requests to the invoice.
  2. Which resource is binding. If nodes are full on memory or on pod count while CPU sits at 30%, cutting CPU requests frees nothing.
  3. Cluster autoscaler scale-down events over the period. Zero scale-downs with falling requests is the whole diagnosis in one graph.

The diagnosis

You do not pay for requests. You pay for nodes. Requests decide packing; nodes decide the bill. Freeing capacity inside a node that nobody removes converts spend into headroom, not savings.

Four mechanisms usually block the removal:

  • The binding dimension is not the one you cut. Many serving fleets are memory-bound and pod-count-bound. Most managed distributions default to about 110 pods per node, and a fleet of small sidecar-heavy pods hits that ceiling long before it hits CPU.
  • Scale-down requires evacuation. The autoscaler removes a node only when every pod on it can be rescheduled elsewhere. A single pod with local storage, a strict PodDisruptionBudget at maxUnavailable: 0, or a kube-system DaemonSet without the eviction annotation pins the node permanently.
  • DaemonSets and reserved capacity are a fixed tax per node. Log shipper, node exporter, CSI driver, mesh proxy and kube-reserved can consume a meaningful slice of a small node. Halving workload requests does not halve that, so the per-node overhead ratio gets worse as you shrink.
  • Anti-affinity and topology spread force pods apart, so the scheduler holds nodes open that packing alone would have emptied.

The misleading signal

The rightsizing dashboard shows request utilisation and it is telling the truth about the wrong thing. Request reduction is an input; node-hours are the output, and the two are connected only through an autoscaler that is allowed to act.

The fix, in order

  1. Publish bin-packing efficiency per node pool and make it the campaign's metric.
  2. Find the pins: list nodes the autoscaler evaluated and declined, with the reason.
  3. Fix the pins — relax PDBs to maxUnavailable: 1, move local-storage workloads, annotate DaemonSets.
  4. Only then resize the node shape to match the new pod shape. This is usually the largest single saving and it is the step teams skip, because it means draining the fleet.
  5. Enable consolidation, where the provisioner actively repacks rather than waiting for an empty node.

The alert that would have caught it earlier

Alert on requested-to-allocatable ratio below 60% for a node pool over 24 hours, not on pod CPU. It fires when capacity is stranded, which is the actual failure.

When this is the wrong answer

On a fleet of a few dozen nodes, the engineering time to fix scheduling pins exceeds the saving. Buy a smaller node shape and move on.