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:
| Strategy | Favors | Effect |
|---|---|---|
LeastAllocated (default) | Nodes with the most free capacity | Spreads Pods, nodes fill evenly |
MostAllocated | Nodes with the least free capacity | Packs Pods, leaves some nodes empty for scale-down |
RequestedToCapacityRatio | A custom curve of requested vs capacity per resource | Tunable 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
HighNodeUtilizationplugin empties underused nodes and must be paired with theMostAllocatedscoring 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:
| Blocker | Why it blocks |
|---|---|
| Restrictive PodDisruptionBudget | An eviction would break the budget, for example maxUnavailable: 0 |
| Local storage | Pods 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 controller | Nothing would recreate them after eviction |
kube-system Pods without a PDB | The cluster autoscaler leaves them alone by default |
| Affinity, anti-affinity or node selectors | No 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.