Karpenter Overprovisioning Prevention Best Practices

I still remember the email. Subject line: "AWS bill hit $127k last month — what happened?" It was early 2025, and our client, a mid-sized fintech I’ll ca...

karpenter overprovisioning prevention best practices
By Nishaant Dixit
Karpenter Overprovisioning Prevention Best Practices

Karpenter Overprovisioning Prevention Best Practices

Stop 3AM Pages

Free K8s Audit

Get Started →
Karpenter Overprovisioning Prevention Best Practices

I still remember the email. Subject line: "AWS bill hit $127k last month — what happened?" It was early 2025, and our client, a mid-sized fintech I’ll call PayFlow, had been running Karpenter for six months. They thought they were doing everything right. They weren't.

The problem? Overprovisioning baked into their Karpenter setup. Not the obvious kind — not leaving idle nodes running. The subtle kind: aggressive provisioning thresholds, loose budget limits, and a near-religious belief that "more capacity is safer." It cost them $25,000 a month in compute they didn’t need.

Overprovisioning isn’t just wasted money. It’s slower cluster responsiveness, more spot interruptions, and harder debugging. Karpenter is fast — sometimes too fast. It will spin up nodes the moment a pod is pending, whether or not that pod really needs its own node. Left unchecked, you get a fleet of half-empty instances running 24/7.

This guide is for teams running Karpenter in production. I’ll walk through the karpenter overprovisioning prevention best practices I’ve learned the hard way — from the provisioning config you set today to the monitoring you absolutely need tomorrow. You’ll see real YAML, real numbers, and real trade-offs. No fluff.

Why Overprovisioning Happens (and Why Most Advice Is Wrong)

Most people think overprovisioning is caused by too many node groups or too large instance types. They’re wrong.

The real culprit is karpenter provisioning configuration for cost being too aggressive on the "time to readiness" axis. Karpenter defaults to provisioning a node as soon as a pod is unschedulable. That sounds good. But if you have batch jobs that run for 30 seconds and then exit, Karpenter will spin up a whole c5.xlarge for them, keep it for minutes, then consolidate it down. Those short-lived nodes cost more than the job’s runtime.

I tested this at SIVARO in 2024. We had a Spark-on-Kubernetes workload that created 50 pods every hour. Each pod ran for ~45 seconds. Default Karpenter config? It provisioned a node per pod, leading to 30x overprovisioning by node count. Fixed it by batching pods onto fewer nodes using pod groups and tuning consolidationPolicy — but more on that later.

Another hidden driver: CPU/memory requests that are wildly inflated. If your devs set 2 CPU requests for a service that uses 0.5 CPU peak, Karpenter will see that as demand and provision accordingly. Overprovisioning starts in the deployment specs, not just the Provisioner.

Understanding Karpenter Provisioning Configuration for Cost

Your Provisioner object is where the battle is won or lost. I’ve seen dozens of Provisioner YAMLs that look like this:

yaml
apiVersion: karpenter.sh/v1beta1
kind: Provisioner
metadata:
  name: default
spec:
  requirements:
    - key: "node.kubernetes.io/instance-type"
      operator: In
      values: ["m5.large", "m5.xlarge", "m5.2xlarge"]
  limits:
    resources:
      cpu: 1000
  providerRef:
    name: default

This is garbage. Here’s why:

  • No consolidation block: Introduced in Karpenter v0.32 (late 2024), consolidation lets you specify whether to consolidate nodes based on WhenEmpty or WhenUnderutilized. Default is WhenEmpty. That means nodes only consolidate when completely empty. You’ll end up with many nodes at 10% utilization if pods aren't perfectly packed.

  • No weightedPrices: Spot instances can be mixed with On-Demand, but without a weightedPrices policy, Karpenter favors the cheapest instance type without considering interruption probability. That leads to constant churn and overprovisioning of fallback On-Demand nodes.

  • Flat limits: Setting a global CPU limit of 1000 is a blunt instrument. It prevents scaling beyond that, but it doesn’t prevent overprovisioning within that limit. You need per-instance limits and consolidation behavior.

The karpenter provisioning configuration for cost I recommend after hundreds of experiments:

yaml
apiVersion: karpenter.sh/v1beta1
kind: Provisioner
metadata:
  name: cost-optimized
spec:
  consolidation:
    enabled: true
    policy: WhenUnderutilized  # Changed in 2025 to actually work
    consolidationTTL: 2m
  requirements:
    - key: "karpenter.sh/capacity-type"
      operator: In
      values: ["spot", "on-demand"]
    - key: "node.kubernetes.io/instance-type"
      operator: In
      values: ["m5.large", "m5.xlarge", "m5.2xlarge", "c5.xlarge", "c5.2xlarge"]
  limits:
    resources:
      cpu: 5000
    nodes: 50
  providerRef:
    name: default
  weight: 10
  ttlSecondsAfterEmpty: 30
  ttlSecondsUntilExpired: 2592000  # 30 days

Why WhenUnderutilized matters

WhenUnderutilized was barely documented until early 2026. It tells Karpenter to consolidate nodes even if they aren’t empty — if total CPU/memory usage across a node stays below 60% for consolidationTTL, move those pods and terminate the node. This directly prevents the "half-empty node" problem.

I’ve seen clusters go from 30% average utilization to 65% just by flipping this setting. That’s a 50% reduction in node count for the same workload. It’s the single most impactful overprovisioning prevention lever.

Setting Realistic Limits: The Sweet Spot

Limits are not a cap on cost. They’re a cap on chaos. Set them too low and your workloads get Pending for no good reason. Set them too high and you’re back to overprovisioning.

How I do it: Start with a ceiling of 150% your peak historical usage. Then add a node count limit equal to your peak node count plus 20% headroom. Combine that with ttlSecondsAfterEmpty: 30 to drain idle nodes fast.

But the real trick? Use namespace-level quotas. Karpenter respects them. If your default namespace has a total CPU request limit of 100, Karpenter won’t provision nodes that would exceed that. This prevents one team from gobbling up all the capacity and your finance team from sending angry emails.

At SIVARO, we layer ResourceQuotas on every namespace with a 30% buffer above actual usage. Works like a charm.

Leveraging Spot Instances Without the Overhead

I’m a huge fan of spot instances. They can cut costs by 60-90% for interruptible workloads. But karpenter spot instance cost optimization requires deliberate planning. The naive approach — let Karpenter use 100% spot — leads to two overprovisioning problems:

  1. Excessive fallback to On-Demand: When spot capacity is low, Karpenter scales up On-Demand nodes to meet demand. If your spot interruption rate is high (e.g., 10% per hour), you’ll see spikes of On-Demand nodes that stay around after the interruption storms pass.

  2. Node churn: Each spot interruption triggers re-provisioning. If consolidation is slow, you accumulate dead nodes waiting for ttlSecondsAfterEmpty.

Here’s how I fixed it at a 2025 client (an e-commerce platform doing 50M monthly requests):

Weighted pricing + interruption budget. Instead of pure spot, I split the fleet: 70% spot, 30% On-Demand. Karpenter picks the cheapest spot instance type that meets requirements, but if the interruption rate exceeds 5% in a sliding 10-minute window, it pauses spot provisioning and uses On-Demand until the window resets.

The implementation uses weightedPrices and a custom metric. Not trivial, but from AWS's blog on Karpenter consolidation, they recommend similar. We built a simple controller that updates a ConfigMap with the current spot health status. Karpenter reads that via requirements:

yaml
- key: "karpenter.sh/capacity-type"
  operator: In
  values: ["spot", "on-demand"]
- key: "sivaro.io/spot-health"
  operator: In
  values: ["ok"]

When spot-health changes to degraded, the controller flips the Provisioner's weight to prefer On-Demand. It’s hacky but works.

A cleaner approach (as of Karpenter v0.41, released April 2026) is the built-in spotInterruptionPolicy field — you can set maximumChurnRate and fallbackOnDemand. I haven’t fully tested it yet, but it’s promising.

Consolidation: Your Best Friend and Worst Enemy

Consolidation: Your Best Friend and Worst Enemy

Understanding Karpenter Consolidation: Detailed Overview provides a great breakdown. I’ll add my own war stories.

Consolidation is on by default (WhenEmpty). Most teams never change it. That’s a mistake.

WhenEmpty means: "Only consolidate a node when it has zero schedulable pods." In practice, that means nodes with one or two pods (e.g., daemonsets, monitoring agents) never consolidate. You end up with a permanent tail of small nodes.

Solution: Combine WhenUnderutilized with ttlSecondsAfterEmpty: 30. And crucially, don’t set ttlSecondsAfterEmpty too high — I’ve seen 5-minute delays causing clusters to balloon during traffic spikes. 30 seconds is enough to drain short-lived jobs.

One more trick: Use nodeClassRef (introduced in v0.38) to separate spot and On-Demand node templates. That way, consolidation can prioritize spot whenever possible. Optimizing your Kubernetes compute costs with Karpenter covers this strategy.

Pod Disruption Budgets: The Safety Net Nobody Configures

I once saw a production outage caused by aggressive consolidation deleting the last instance of a stateful application. That app had no PodDisruptionBudget (PDB). Consolidation saw the node was underutilized, terminated it, and the application became unavailable for 45 seconds until the new node spun up.

A Personal Take on Pod Disruption Budgets and Karpenter nails it: PDBs are not optional when using Karpenter. They protect your workloads from both consolidation and spot interruptions.

Best practice: Set minAvailable or maxUnavailable for every workload that matters. I use:

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api

For stateless workloads, maxUnavailable: 1 is fine. For stateful (e.g., Redis, Kafka brokers), minAvailable: <quorum> is non-negotiable.

Karpenter respects PDBs during consolidation. If a node has pods with a PDB preventing eviction, that node won’t be consolidated until the PDB allows it. This prevents overprovisioning from the safety side — you don’t need to overprovision "just in case" because Karpenter won’t kill all replicas at once.

Monitoring and Alerting: What to Watch

You can’t prevent overprovisioning if you can’t see it. I ship the following metrics to Prometheus:

  • karpenter_nodes_created / karpenter_nodes_terminated – if creation rate >> termination rate for more than 5 minutes, you’re overprovisioning.
  • karpenter_consolidation_decision_total – track how many nodes are being consolidated per hour. If it’s consistently zero, your consolidation settings are wrong.
  • node_cpu_utilization averaged over 5 minutes – target 60-70%. Below 50% means overprovisioning.

We built a Grafana dashboard that flags any node below 40% utilization for more than 10 minutes. That’s our "overprovisioning alert."

Kubernetes Cost Optimization: A 2026 Guide to Reducing … suggests adding cost allocation labels. I tag every node with sivaro.io/provisioner and sivaro.io/workload to trace spend back to teams.

Advanced: Custom Node Templates and Weighted Pricing

NodeTemplates are underutilized. Most teams use one template for all workloads. That forces Karpenter to use the same instance profile, security groups, and block devices for everything. Not cost-efficient.

I separate templates by workload family:

  • Batch jobs: Large ephemeral storage, spot only, ttlSecondsAfterEmpty: 5
  • Web serving: Small instances, On-Demand + spot mix, consolidationTTL: 1m
  • Stateful stores: Dedicated instances, no spot, no consolidation

Each template has its own Provisioner weight. Karpenter picks the cheapest option that meets the pod's requirements. This naturally prevents overprovisioning because batch jobs don’t pollute the web pool.

Weighted pricing can be implemented via karpenter.sh/provisioner-weight label. I set high weights for spot Provisioners, lower for On-Demand. That way, Karpenter prefers spot unless unavailable.

FAQ

Q: What’s the single fastest way to reduce overprovisioning today?
A: Enable consolidation.policy: WhenUnderutilized with a 2-minute consolidationTTL. I’ve seen 30-50% node count drops in 24 hours.

Q: Should I set ttlSecondsAfterEmpty to 0?
A: No. That can cause infinite consolidations of the same node. 30 seconds is the sweet spot.

Q: How do I handle spot interruptions without overprovisioning On-Demand fallback?
A: Use a spot health window and a maximum interruption rate threshold. Pause spot provisioning when rate exceeds 5%/10min.

Q: Does Karpenter support horizontal pod autoscaling integration?
A: Indirectly. HPA triggers pod scale, Karpenter provisions nodes. Set your HPA target to 70% utilization to avoid creating pods too early.

Q: What about GPU overprovisioning?
A: GPU instances are expensive. Always use explicit resources.limits and resources.requests for GPU pods. Never use default Provisioner for GPU — create a dedicated one with spot disabled.

Q: Is it better to overprovision slightly or underprovision?
A: Underprovision by 10% and rely on Karpenter fast scaling (~30 seconds). Overprovisioning by 10% wastes 10% of your budget. Over 3 months, that’s real money.

Q: How often should I review my Provisioner config?
A: Every month. Workloads change. Instance types change. Prices change. Your config should too.

Conclusion

Conclusion

Karpenter overprovisioning prevention best practices aren’t about avoiding scaling. They’re about scaling intelligently. Set consolidation to WhenUnderutilized, configure PDBs, use weighted spot pricing, and monitor utilization religiously.

At SIVARO, we helped that fintech client cut their $127k bill to $82k in two weeks — just by fixing their Karpenter config. No application changes. No infrastructure redesign. Just better defaults and a few YAML tweaks.

Your turn. Go look at your Provisioner. Is consolidation set to WhenEmpty? Do you have a ttlSecondsAfterEmpty lower than 30? If so, you’re paying for it. Fix it today.

Tomorrow? Fine-tune your spot interruption handling and add namespace quotas. Overprovisioning isn’t a one-time fix; it’s a continuous practice. But you can get 80% of the benefit with three changes: better consolidation, realistic limits, and pod disruption budgets.


Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Free · No Commitment · 48-Hour Delivery

Get a free infrastructure audit

2-hour remote session. We audit your data infrastructure, identify what's costing you time and money, and deliver a written roadmap with specific, measurable targets. No pitch.

Book Your Free Audit
N
Nishaant Dixit
Founder & Lead Engineer at SIVARO

Building data-intensive systems since 2018. 200K events/sec pipelines, production RAG systems, Kubernetes infrastructure. LinkedIn →

Start a Project
Need help with infrastructure?

Kubernetes, Karpenter, DevOps pipelines, and container orchestration for production workloads.

Explore MVP to Production