What is bin packing in Kubernetes, and why do nodes stay half empty?

Bin packing in Kubernetes is placing Pods onto as few nodes as possible, so that the remaining nodes can be removed and stop costing money. Nodes stay half empty because the default scheduler spreads Pods across nodes instead of filling them, and because Kubernetes never moves a running Pod to a better node on its own. Consolidation, which evicts Pods from underused nodes and removes those nodes, is what turns a packed schedule into a smaller bill.

Why the default scheduler spreads Pods

The kube-scheduler scores every node that can fit a Pod and picks the highest score. Its NodeResourcesFit plugin uses the LeastAllocated strategy by default, which scores nodes with the most free CPU and memory highest. That spreads load and leaves headroom on every node, which is good for availability and bad for density.

The plugin supports two bin packing strategies:

StrategyFavorsEffect
LeastAllocated (default)Nodes with the most free capacitySpreads Pods, nodes fill evenly
MostAllocatedNodes with the least free capacityPacks Pods, leaves some nodes empty for scale-down
RequestedToCapacityRatioA custom curve of requested vs capacity per resourceTunable between spreading and packing, with weights per resource

Changing the strategy needs access to the scheduler configuration, which many managed services do not expose. And scoring only affects new Pods. Pods already running stay where they are.

Fragmentation and node shape

Even with packing, capacity gets stranded when Pod sizes and node sizes do not line up:

  • Leftover slivers. A node with 4 vCPU fits two Pods requesting 1.5 vCPU each. The last 1 vCPU sits idle because no waiting Pod is that small.
  • Mismatched ratios. Nodes with 4 GiB of memory per vCPU run memory-heavy Pods that need 8 GiB per vCPU. Memory fills first and half the CPU is never requested.
  • Per-node overhead. Every node runs DaemonSets for networking, logging and monitoring, and reserves capacity for the kubelet and the operating system. Many small nodes pay that overhead many times.

The fix is choosing node shapes that match the requests of the Pods that need them, which is easier when nodes are created per workload than when a fixed node pool serves everything. What a Kubernetes node is explains allocatable capacity and reservations.

Consolidation

Scheduling is a one-time decision. After a scale-down or a batch of finished jobs, a cluster can run ten nodes at 30% when six would do. Three tools address this:

  • Cluster autoscaler scale-down. The cluster autoscaler marks a node as unneeded when both the summed CPU requests and the summed memory requests of its Pods stay below 50% of allocatable (--scale-down-utilization-threshold) for 10 minutes and its Pods can move elsewhere. It then drains and removes the node.
  • The descheduler. The descheduler evicts Pods so the scheduler can place them again. Its HighNodeUtilization plugin empties underused nodes and must be paired with the MostAllocated scoring strategy.
  • Node auto-provisioning. Instead of scaling fixed node pools, a node auto-provisioner creates nodes sized to pending Pods and can replace several underused nodes with fewer or smaller ones.

What blocks consolidation

A node can only be removed when every Pod on it can be evicted and placed elsewhere. These keep nodes alive:

BlockerWhy it blocks
Restrictive PodDisruptionBudgetAn eviction would break the budget, for example maxUnavailable: 0
Local storagePods using hostPath or a disk-backed emptyDir lose data when moved
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"Explicitly opts the Pod out of eviction
Pods without a controllerNothing would recreate them after eviction
kube-system Pods without a PDBThe cluster autoscaler leaves them alone by default
Affinity, anti-affinity or node selectorsNo other node satisfies the constraints

DaemonSet Pods do not block removal, but they count toward the node’s requested resources by default, which can keep a nearly empty node above the threshold.

Density vs blast radius

Packing tighter saves money and raises risk. Fewer, larger nodes cut per-node overhead, but losing one node takes down more Pods at once, and a fully packed node has no headroom for bursts. Keep replicas of each service on different nodes with topology spread constraints, keep PodDisruptionBudgets that allow at least one eviction, and leave headroom in requests for workloads that burst.

Packing works on requests, so oversized requests limit what it can save. Pair it with rightsizing and accurate resource requests. How Kubernetes autoscaling works covers how node and Pod autoscaling interact.

How Cloudfleet handles it

Cloudfleet Kubernetes Engine (CFKE) consolidates Fleets through node auto-provisioning. The conservative profile, the default, removes nodes that are already empty, and the aggressive profile also evicts running Pods to repack them onto fewer or smaller nodes. Each Fleet has its own profile, so stateless and long-running workloads can use different policies in one cluster.

On this page