kubernetescost-optimizationfinopsutilization

Kubernetes bin packing: turning 35 percent utilization into 70 percent

Most Kubernetes clusters run at 25 to 40 percent CPU utilization, which means well over half the node bill buys nothing. Bin packing is the discipline of fitting pods onto fewer nodes. Here is how the scheduler decides and what to change.

The C3X Team··7 min read

Quick answer

Bin packing is how efficiently pods fill nodes, and it is the single biggest source of Kubernetes waste. Average cluster CPU utilization sits at 25 to 40 percent in most estates, meaning 60 to 75 percent of node spend produces nothing. The main causes are inflated resource requests, the default LeastAllocated scheduler policy that spreads pods thin, anti-affinity rules, and node sizes that do not divide evenly into pod shapes. Switching to the MostAllocated scheduler profile, right-sizing requests, and consolidating node pools typically lifts utilization from about 35 percent to 65 to 75 percent, cutting the node bill roughly in half.

Kubernetes bills you for nodes, not pods. Whether a node runs at 90 percent or 9 percent, it costs the same $280 per month. Bin packing is the question of how many nodes you actually need to hold the pods you actually have, and in most clusters the honest answer is about half the number running.

What utilization actually looks like

MetricTypical clusterWell-packed cluster
CPU requested vs allocatable45 to 60 percent75 to 85 percent
CPU actually used vs allocatable25 to 40 percent55 to 70 percent
Memory used vs allocatable35 to 50 percent65 to 80 percent
Node count for same workload4022

There are two gaps here and they compound. The first is between what pods request and what they use, therequests versus usage gap, typically 2 to 3 times. The second is between what pods request and what the cluster provisions, which is the bin packing gap. A cluster where pods request 2.5 times what they use and the scheduler packs nodes to 50 percent of requests is running at roughly 20 percent real utilization, paying 5 dollars for 1 dollar of work.

The scheduler spreads by default

The default kube-scheduler scoring plugin, NodeResourcesFit with the LeastAllocated strategy, prefers nodes with the most free resources. That is a reasonable default for latency and blast radius and a terrible one for cost: it deliberately spreads pods across as many nodes as possible, which means no node ever becomes empty enough for the autoscaler to remove.

Switching the scheduler profile to MostAllocated inverts this: pods prefer the fullest node that still fits. Combined with a cluster autoscaler set to a reasonable scale-down utilization threshold, nodes drain and terminate. In a 40-node cluster where pods request 55 percent of allocatable capacity, moving to MostAllocated commonly lands the same workload on 25 to 28 nodes. At $280 per node that is $3,360 to $4,200 per month.

What blocks packing

BlockerEffectFix
Inflated requestsFewer pods fit per nodeSet requests from p95 usage
LeastAllocated scoringPods spread thinMostAllocated profile
Pod anti-affinityForces one pod per nodeUse topology spread with maxSkew
Missing PodDisruptionBudgetsAutoscaler refuses to drainDefine PDBs that allow eviction
Node shape mismatchStranded capacity per nodeSize nodes to pod multiples
Long-running jobsPin a node open for hoursSeparate pool for batch

Anti-affinity deserves special mention. A blanket requiredDuringSchedulingIgnoredDuringExecution anti-affinity on hostname means a Deployment with 20 replicas occupies 20 nodes no matter how small the pods are. Twenty 0.25 vCPU pods could share two nodes. Instead they hold 20 nodes open at $280 each, $5,600 per month for $560 worth of compute. topologySpreadConstraints with maxSkew gives you the availability property without the one-pod-per-node tax.

PodDisruptionBudgets are the second most common blocker and the least obvious. A PDB with minAvailable equal to the replica count permits zero voluntary evictions, so the autoscaler can never drain those pods and can never remove the node under them. Three such services scattered across a cluster keep three nodes permanently alive, $840 per month, for a setting that was intended to protect rolling deploys rather than to pin infrastructure. Setting minAvailable to replicas minus one, or using maxUnavailable of 1, preserves the availability intent and restores consolidation.

The consolidation loop

Packing is not a one-time fix because workloads churn. The durable version is a loop: right-size requests from observed p95 usage, let the scheduler pack aggressively, let the autoscaler consolidate, and repeat monthly. Karpenter's consolidation feature automates the node side by actively replacing under-used nodes with smaller or fewer ones, which is the mechanical advantage it holds over the classicCluster Autoscaler.

A worked example

A cluster runs 600 pods with total requests of 480 vCPU and observed p95 usage of 190 vCPU. It runs 60 m5.2xlarge nodes, 432 allocatable vCPU, at $16,800 per month, and the nodes sit at 31 percent real utilization.

StepRequested vCPUNodes neededMonthly cost
Baseline48060$16,800
Requests set to p95 plus 30 percent24740$11,200
MostAllocated packing to 80 percent24729$8,120
Anti-affinity replaced with spread24725$7,000

That is a 58 percent reduction with no change to what the applications do. The workload is identical, the latency is unchanged, and the only thing that moved was how honestly the cluster described its own needs. Packing efficiency is measurable, so track requested vCPU divided by allocatable vCPU as a cluster KPI and set a target above 70 percent. Price node group changes from Terraform against theresource catalog so the savings from consolidation show up as a number before the nodes go away.

FAQ

What is bin packing in Kubernetes?

Bin packing is how efficiently pods fill the nodes they run on. Kubernetes bills you per node, so a node running at 9 percent costs the same as one running at 90 percent. Good bin packing means fitting the same pods onto fewer nodes by right-sizing requests, using a packing-oriented scheduler profile, and removing constraints that force pods to spread.

What is typical Kubernetes cluster utilization?

Most clusters run at 25 to 40 percent CPU utilization against allocatable capacity, with memory typically at 35 to 50 percent. Well-packed clusters reach 55 to 70 percent CPU and 65 to 80 percent memory. The gap comes from two compounding sources: pods requesting two to three times what they use, and the scheduler spreading pods across more nodes than necessary.

How do I make the Kubernetes scheduler pack pods more tightly?

Switch the NodeResourcesFit scoring strategy from the default LeastAllocated to MostAllocated in a scheduler profile. LeastAllocated prefers nodes with the most free capacity, deliberately spreading pods so no node ever empties enough to remove. MostAllocated prefers the fullest node that still fits, which lets the autoscaler drain and terminate nodes. This alone often cuts node count by 30 percent.

Does pod anti-affinity increase Kubernetes cost?

Yes, substantially. A required anti-affinity rule on hostname means a Deployment with 20 replicas occupies 20 separate nodes regardless of pod size. Twenty 0.25 vCPU pods that could share two nodes instead hold 20 nodes at roughly $280 each, $5,600 per month for $560 of actual compute. Use topologySpreadConstraints with maxSkew to get availability spreading without the one-pod-per-node penalty.

How much can better bin packing save?

Commonly 40 to 60 percent of the node bill. A worked example: a cluster with 480 vCPU requested but 190 vCPU actually used ran 60 nodes at $16,800 per month. Setting requests from p95 usage dropped it to 40 nodes, switching to MostAllocated packing reached 29 nodes, and replacing blanket anti-affinity with topology spread landed at 25 nodes and $7,000, a 58 percent cut with no application change.

How does C3X help with Kubernetes packing efficiency?

C3X prices node groups from Terraform against a live catalog, so the cost of a node count or instance type is explicit before merge. When a consolidation effort reduces a node group from 60 to 25 instances, the saving appears as a concrete monthly figure in the pull request, which makes packing work easy to justify and easy to track over time.

What to do next

Turn packing wins into numbers. C3X reads your Terraform and prices your resources against a live catalog. Start with the quickstart.

Try C3X on your own Terraform

Free and open source. No API key required. One command to install, one command to estimate.