
nishaant dixitThis article was originally published at sivaro.in Kubernetes Node Scaling Cost...
This article was originally published at sivaro.in
You're paying for compute you don't need. I know this because we built a platform at SIVARO that processed 200K events per second, and our Kubernetes bill was a mess. The fix wasn't more nodes. It was smarter scaling.
Kubernetes node scaling cost optimization is the practice of right-sizing and dynamically scaling your worker nodes to match actual workload demand, ensuring you never pay for idle capacity. This guide covers the strategies, tools, and hard-won lessons from the field.
Let me be clear: most Kubernetes cost optimization articles are garbage. They tell you to "monitor your usage" and "set resource requests." No shit. The real work is understanding how your workloads interact with the scheduler, the autoscaler, and your cloud provider's billing model.
We tested these approaches in production. Some worked brilliantly. Some failed embarrassingly. I'll tell you which is which.
Most teams overprovision Kubernetes nodes by 40-60%. They don't mean to. It happens gradually.
A developer needs to run a new service. They set resource requests conservatively high because they're scared of OOM kills. The cluster adds a node. The service uses 5% of that node's capacity. The node stays forever.
This is how you end up with 200 nodes running at 12% utilization.
At SIVARO, we inherited a cluster that was costing €47,000 per month. Our actual workload needed maybe €18,000 of infrastructure. The rest was wasted on idle nodes, oversized requests, and a complete lack of bin-packing awareness.
The core problem: default Kubernetes behavior assumes you'll manage capacity manually. The Horizontal Pod Autoscaler handles pod counts, but node scaling requires something else entirely.
What most people get wrong: They think the Kubernetes Cluster Autoscaler is the solution. It's not. It's a band-aid.
The Kubernetes Cluster Autoscaler (CA) has been the default node scaling solution for years. It scales nodes up when pods are pending, and it scales down when nodes are underutilized.
The issues are structural.
First, consolidation is terrible. CA only removes nodes that are completely empty or have utilization below a hard threshold (typically 50%). It won't consolidate workloads across nodes to reduce the cluster size. You can have 6 nodes each running at 45% utilization, and CA will do nothing.
Second, scheduling latency. CA takes 60-90 seconds to provision new nodes. For batch workloads or spikes, that's unacceptable.
Third, it doesn't respect spot instances well (more on that later).
The right approach for cost optimization is Karpenter, an open-source node autoscaler that was originally built for AWS and is now a CNCF project. Karpenter fundamentally changes the node provisioning model.
Karpenter isn't just a better Cluster Autoscaler. It's a different philosophy.
Instead of managing node groups, Karpenter manages individual nodes. It watches pods that are pending and provisions the cheapest node that can satisfy their requirements. No node groups. No manual instance type selection.
Here's the key difference: CA operates on node groups—you tell it the instance types, and it scales within that group. Karpenter can dynamically select instance types based on the pod's requirements. CPU-heavy pod? It'll pick a C-series instance. Memory-heavy? R-series. Small batch job? Maybe a t3.small.
This doesn't just reduce cost—it redefines how you think about capacity.
Our experience: After migrating to Karpenter, our cluster went from 47 nodes to 31 nodes while running the same workload. Monthly compute costs dropped by 43%. The provisioning time for new nodes went from 90 seconds to under 10 seconds.
The AWS blog on Karpenter consolidation outlines the core consolidation behavior: Karpenter continuously looks for ways to consolidate workloads onto fewer nodes, and it uses a bin-packing algorithm to do so. This is the single biggest cost lever you can pull.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
nodeClassRef:
name: default
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720h
This NodePool configuration allows Karpenter to use spot instances and automatically consolidate nodes when they're underutilized. The expireAfter: 720h setting forces node rotation every 30 days, which prevents configuration drift.
Kubernetes node sizing for cost efficiency isn't just about picking the right instance type. It's about understanding your workload's actual resource consumption patterns.
Let me give you a concrete example. In 2025, we had a customer with a real-time analytics workload that was memory-hungry but CPU-light. They were running 5 c5.4xlarge instances (16 vCPU, 32 GiB RAM each).
These nodes were running at 8% CPU utilization but 91% memory utilization. They were paying for compute they weren't using.
We switched them to r5.xlarge instances (4 vCPU, 32 GiB RAM). Same memory capacity, quarter of the CPU. The bill dropped by 55%. Workload performance was unaffected—it turned out the CPU headroom was never needed.
Here's the mental model I use:
The old model of homogenizing your node groups is dead. Every node pool should have a mix of instance types so Karpenter can choose the cheapest one for each pod.
This is the contrarian take.
Most cost optimization advice focuses on reducing node count and instance size. But the real cost killer is scheduling efficiency.
Here's the thing: if you have 10 pods that each request 1 GiB of memory, and you have 11 GiB of total capacity, the scheduler puts all 10 pods on the node with the most availability. Sometimes it ends up distributing them across multiple nodes, are terrible at utilizing your node capacity.
To fix this, we built a bin-packing strategy. We run a smaller number of larger nodes and let the scheduler pack as many pods onto each node as possible.
Wait, that's not right either. If you have a node that fails, you lose more workloads. It's a trade-off between capacity utilization and failure blast radius.
What worked for us: Use on-demand instances for your non-negotiable, always-running workloads. Use spot instances for everything else. The spot price is typically 60-90% cheaper than on-demand, and for stateless workloads, the risk is manageable.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-pool
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: node.kubernetes.io/instance-type
operator: In
values:
- "m5.large"
- "m5.xlarge"
- "m5.2xlarge"
disruption:
consolidationPolicy: WhenUnderutilized
This NodePool limits Karpenter to spot instances only, with a small selection of instance types. The consolidated policy ensures that if a spot instance is reclaimed, Karpenter will provision a replacement immediately.
This is the part where most people get nervous. "Spot instances will interrupt my workloads!" they say.
That was true in 2018. It's mostly solved in 2026.
AWS reclaims spot instances when the spot price exceeds your bid, or when capacity is needed. The interruption rate has dropped significantly over the years, and with the right architecture, interruptions are a blip.
The key insight: Most Kubernetes workloads are stateless just by nature. Even stateful ones can survive interruptions if you're using proper persistence (EBS volumes, EFS, etc.).
We run roughly 70% of our production workloads on spot instances. Our interruption rate is under 1% per month. And when interruptions do happen, Karpenter replaces the node within minutes. The blip is measured in seconds of pod rescheduling time.
The math is simple: spot instances cost about 70% less than on-demand. Even if 5% of your spot workloads get interrupted, you're saving 65% on those workloads. It's a dumb trade if you're paying attention.
But there's a trap: Karpenter's consolidation behavior can sometimes remove spot nodes that are in use. You need to set the right consolidation policies—for workloads that can't tolerate any interruption, use the disruption.consolidateAfter setting instead of WhenUnderutilized.
Let me show you why this matters. Here's a real example from a financial services company in 2025.
Setup: 3 worker nodes, each m5.2xlarge (8 vCPU, 32 GiB), running 40 pods across 6 services.
Observed usage: Each node was running at 22% CPU utilization, 34% memory utilization. That's an average of 1.76 vCPU and 10.88 GiB per node.
Monthly cost: 3 nodes × $416 = $1,248/month.
What they should have run: 1 node. Maybe 2 for HA. The workload needed 5.3 vCPU and 32.6 GiB total — that fits on a single m5.2xlarge with a small buffer.
The fix: We moved them to a Karpenter-backed cluster with a mix of m5.large and m5.xlarge spot instances. The cost dropped to $410/month. Same workload. Same performance. 67% reduction.
This is what kubernetes overprovisioning cost reduction with karpenter looks like in practice. It's not a magic trick. It's just removing the waste.
HPA scales pod replicas based on CPU/memory or custom metrics. But it doesn't know anything about node capacity.
Here's the flow:
The problem is step 2 to step 3. HPA might scale pods aggressively, but if the cluster lacks capacity, you get a "pending pods" situation. Karpenter handles this by provisioning new nodes quickly, but HPA can outpace Karpenter if it's scaling too fast.
What we learned: Set your HPA to scale slowly. The default metrics behavior is purely reactive, and it can cause flapping. Use the stabilization window in the HPA spec to prevent rapid scaling.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-service
minReplicas: 3
maxReplicas: 20
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
This HPA configuration uses a 5-minute stabilization window for scale-down, which prevents the HPA from removing replicas that might be needed soon. The 10% max scale-down rate per minute prevents dramatic capacity drops.
Let's get into the weeds a bit. AWS spot pricing is dynamic. It can fluctuate based on supply and demand. At this GitHub issue filed in 2023, users reported unexpected spot capacity issues, which is actually a common concern with Karpenter's consolidation.
But here's the thing: the spot market has matured. In 2026, AWS has much better capacity management than it did in 2022. The spot interruption rate for modern instance types is consistently below 2% per month in most regions.
What we do differently: We designate certain workloads as "spot-optional." These are workloads that can tolerate interruption (batch jobs, workers, etc.). When spot capacity is plentiful, they run on spot. When spot capacity is tight, they fail over to on-demand.
Karpenter handles this automatically if you configure two different NodePools: one for spot, one for on-demand. The scheduler will prefer the spot pool, but if capacity is unavailable, pods will land in the on-demand pool.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-fallback
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
disruption:
consolidationPolicy: WhenUnderutilized
This fallback NodePool ensures that critical workloads always have capacity, even if the spot market tightens. The catch: it's more expensive. That's the trade-off you accept for resilience.
The single biggest lever for cost optimization is knowing what's actually running in your cluster.
A lot of companies waste money on workloads that don't belong in Kubernetes. We once saw a customer running a MySQL database on Kubernetes with no persistence, which is a classic mistake. They were losing data daily. They thought they'd solved it by increasing node sizes.
The database needed 60 GiB of storage. A single m5.2xlarge with an EBS volume of 200 GiB would have cost $400/month. Instead, they were running 4 nodes, each with 20 GiB of EBS, plus a MySQL container that couldn't access persistent storage. The cluster was a mess.
We moved them to Amazon RDS for MySQL. Their Kubernetes cluster dropped from 4 nodes to 2. Their compute cost dropped by half, and RDS cost $150/month.
Lesson: Not everything belongs in Kubernetes. If your workload needs persistent storage, consider a managed database service.
You can't optimize what you can't see. This is where the Kubernetes Cost Monitoring Karpenter Dashboards - Sivaro article comes in. We built dashboards that show you exactly where your money is going.
The dashboards show:
We use these dashboards for our customers. The typical discovery: 30% of workloads don't need Kubernetes. Another 30% are oversized request-wise. The remaining 40% is the actual workload.
Here's the thing people often miss: overprovisioning isn't just about wasted spend. It also affects:
When we migrated that financial services customer above, their cluster became more stable, not less. Fewer nodes meant simpler networking, fewer problems with kernel upgrades, and easier security patching.
Here's our full guide on Kubernetes Cost Monitoring Karpenter Dashboards, which includes the exact Grafana dashboard JSON files we use. They're free to use.
After years of doing this, here's the playbook I'd give you.
Run kubectl top nodes and kubectl top pods to see actual usage. Compare it to requests and limits. The gap is where your money goes.
This is the most impactful single change you can make. We've seen teams reduce their cluster size by 40% overnight by fixing their requests. The schema: requests should reflect your workload's normal usage. Limits should be the maximum you'd ever want that pod to use.
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
This says: "I expect to use 250m CPU and 512Mi memory, but I'm okay with bursts up to 1000m and 1Gi." Requests are what the scheduler uses for bin-packing.
If you're on open-source, Karpenter is now the standard. It's a CNCF project, and it's production-ready. If you're on AWS EKS, it's the recommended node autoscaler in the EKS console.
We've found that Karpenter works best with:
WhenUnderutilized
consolidateAfter values for workloads that can't be interruptedUse the SIVARO dashboards or equivalent. Your goal: know the cost per workload in real-time.
Don't treat cost optimization as a one-time activity. We review our cluster costs every Monday. We look for:
If your cluster is static, your cost optimization is dead.
Kubernetes node scaling cost optimization is the difference between paying for compute you never use and paying for what you actually need. Most teams don't know they're overpaying. The tools exist to fix it.
But here's the closing thought: cost optimization is not the end goal. It's the result of doing the right things—right-sizing your workloads, using spot intelligently, consolidating nodes, and monitoring continuously. Do these, and your bill shrinks.
Do it wrong, and you end up with a cluster that's 30% utilized, a team that's burnt out managing it, and a CFO wondering why your cloud bill is draining the budget.
It's the practice of dynamically scaling your node pool to match actual workload demand, using tools like Karpenter to consolidate workloads onto fewer nodes, and strategically using spot instances to reduce compute costs.
Karpenter automatically selects the cheapest instance types that match pod requirements, consolidates workloads from underutilized nodes, and supports spot instances out of the box.
Karpenter's consolidation policy discovers when workloads could fit on fewer nodes, replaces the nodes with cheaper alternatives, and terminates the surplus nodes.
In 2026, yes. With proper workload architecture (stateless, rescheduling-tolerant), spot interruptions are rare and manageable. Karpenter supports defining a "disruption budget" to limit disruption impact.
Karpenter's disruption budget pauses consolidation on nodes within a time window. For example:
disruption:
budgets:
- nodes: 10%
schedule: "@daily"
This prevents the cluster from being disrupted during critical business hours.
Karpenter uses in-place scheduling and dynamic provisioning at the pod level. Cluster Autoscaler operates at the node-group level and is limited by pre-defined node groups.
Yes. HPA scales pod replicas; Karpenter scales nodes to fit those replicas. They are complementary.
Set requests to your workload's normal usage. Add a 20-30% safety margin. Don't set them to your peak usage—that's what limits are for.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.