How to Set Karpenter Limits to Control Spending
I spent $47,000 on Kubernetes nodes in a single month last year. Not because we needed them — because I trusted Karpenter's default behavior and forgot to set limits. That was a painful lesson, and it's why I'm writing this guide today. Karpenter is a brilliant piece of infrastructure. It's fast, it's smart, and it will happily spin up a p4d.24xlarge for your cron job if you let it. The problem isn't Karpenter. The problem is that most people don't know how to set Karpenter limits to control spending before they deploy it.
If you're reading this on July 30, 2026, you're probably running Karpenter v1.0 or later. The community has matured, the docs have improved, and tools like Cast AI vs ScaleOps vs StormForge vs Kubecost can help monitor it. But none of those tools replace the fundamental discipline of setting guardrails directly in your Karpenter configuration. This article will walk you through exactly how to do that — with code examples, real numbers, and hard-won trade-offs.
Why Karpenter Without Limits Is a Budget Bomb
Karpenter's core promise is autonomy. You define a provisioner, it watches pending pods, and it launches the cheapest EC2 instance that meets your resource requests. Sounds great. But "cheapest" is relative. For a pod requesting 4 vCPU and 8 GB memory, Karpenter might choose a t3.xlarge at $0.1664/hr or a c5.xlarge at $0.17/hr. That's fine. The problem starts when your pod requests 64 vCPU and 512 GB memory, and Karpenter picks an x2gd.16xlarge at $4.0/hr because it's the only instance that fits. One pod consuming $3,000/month. And Karpenter will keep it running until consolidation decides to drain it — which might be never if no cheaper alternative fits.
I've seen this happen at a mid-stage fintech company. They deployed Karpenter, loved the speed, and two weeks later their AWS bill jumped 300%. The issue? A team had deployed a data-processing job with requests: { cpu: "16", memory: "64Gi" } and no node selector. Karpenter launched a c5.4xlarge. Then the job's memory grew. Karpenter scaled up to a c5.9xlarge. Then another pod joined. Within a week they had six c5.18xlarge instances running. Nobody had set any limits on instance types or budgets.
The root cause isn't greed. It's trust. Karpenter trusts your pod requests. If you don't set limits on Karpenter itself, it will spend whatever it takes to schedule your pods. That's both its superpower and its kryptonite.
The Three Levers: NodeTemplate, Provisioner, and EC2NodeClass
Before we dive into specific limits, you need to understand the three configuration objects that control Karpenter's behavior. In Karpenter v1.0 (released early 2025), the old Provisioner resource was replaced by NodePool, and AWSNodeTemplate became EC2NodeClass. If you're on an older version, the concepts are the same — just different resource names.
1. EC2NodeClass — Defines the AWS infrastructure: subnets, security groups, AMI family, instance profile, and block device mappings. This is where you restrict instance families by setting amiFamily, instanceProfile, and optionally securityGroups. You can also set blockDeviceMappings to control root volume size. Costs come from instance types, not from the NodeClass directly, but the NodeClass defines the pool of instances Karpenter can pick from.
2. NodePool — The core scheduling policy. It contains spec.template.spec.requirements for instance types, zones, architectures, and spec.limits.resources for max CPU, memory, and ephemeral storage across all nodes under this pool. This is your primary cost control lever. You can also set spec.consolidation policies, spec.weight for priority, and spec.disruption settings.
3. NodeClaim — The runtime object representing a single node. You don't configure limits here; it's a reflection of what Karpenter has launched. But you can inspect NodeClaims to see what instances were chosen and whether they're within your bounds.
Most people think limits are only about NodePool.spec.limits.resources. That's the blunt instrument. The real finesse comes from layering restrictions on instance types, families, and consolidation behavior.
Setting Limits on Instance Types and Families
The most effective way to control spending is to carve out an allowed set of instance types. Don't let Karpenter choose from all 600-odd EC2 instance types. Pick a shortlist. I recommend starting with 3-5 families that match your workload profile.
A typical compute-optimized set for a web app:
yaml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: compute-optimized
spec:
template:
spec:
requirements:
- key: "node.kubernetes.io/instance-type"
operator: In
values:
- "c6i.large"
- "c6i.xlarge"
- "c6i.2xlarge"
- "c6i.4xlarge"
- "c6i.8xlarge"
- "c7i.large"
- "c7i.xlarge"
- "c7i.2xlarge"
- "c7i.4xlarge"
- "c7i.8xlarge"
- key: "topology.kubernetes.io/zone"
operator: In
values:
- "us-east-1a"
- "us-east-1b"
limits:
resources:
cpu: 100
memory: 400Gi
consolidation:
enabled: true
policy: WhenUnderutilized
Notice I explicitly listed every size from large to 8xlarge. That's intentional. If you use a wildcard like c6i.* (Karpenter doesn't support glob patterns, but you could use In with a range), you'd allow c6i.16xlarge and up — which can be expensive. Be explicit. List the sizes that map to your pod resource profiles.
Why this works: By restricting to c6i and c7i families, you avoid GPU instances, memory-optimized behemoths, and high-density storage instances. The consolidation policy then ensures Karpenter tries to pack workloads into the smallest instance that fits, shrinking overprovisioned nodes.
I tested this pattern at SIVARO on a batch-processing cluster. We had been using a single provisioner with no instance-type restrictions. After restricting to c6i and r6i (memory-optimized), our node count stayed the same but our monthly cost dropped from $18,000 to $11,200. That's a 38% reduction — just by removing the ability to spin up expensive outlier instances.
Controlling Spend with Budget Constraints and Taints/Tolerations
Instance-type restrictions are powerful, but they don't stop Karpenter from launching too many nodes. For that, you need limits.resources. This is a hard cap on the total allocatable CPU and memory across all nodes managed by a given NodePool.
Example with hard limits:
yaml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default-pool
spec:
limits:
resources:
cpu: 50
memory: 100Gi
template:
spec:
requirements:
- key: "node.kubernetes.io/instance-type"
operator: In
values:
- "t3.medium" # cheap burstable for development
- "t3.large"
- "t3.xlarge"
- "m6i.large"
- "m6i.xlarge"
nodeClassRef:
name: default-ec2-nodeclass
consolidation:
enabled: true
policy: WhenEmpty
Set cpu: 50 meaning 50 vCPUs max allocatable across all nodes in this pool. If workloads exceed that, Karpenter will refuse to launch new nodes and pods will stay Pending. That's better than surprise spend. You can pair this with cluster-autoscaler for overflow onto spot instances in a different NodePool, but I prefer to use Karpenter exclusively if possible.
But here's the trap: limits.resources are cluster-wide per NodePool. They don't account for node overhead. A node with 4 vCPU might only have 3.7 vCPU allocatable. So if you set a limit of 50 vCPU, you might actually launch 14 nodes, not 13. Account for overhead. I typically set limits at 90% of what I'd theoretically allow.
Another smart pattern: use taints and tolerations to isolate expensive workloads into a separate NodePool with stricter limits.
yaml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: gpu-pool
spec:
limits:
resources:
cpu: 16
memory: 64Gi
template:
spec:
requirements:
- key: "node.kubernetes.io/instance-type"
operator: In
values:
- "g4dn.xlarge"
- "g4dn.2xlarge"
- "g4dn.4xlarge"
taints:
- key: "gpu"
value: "true"
effect: "NoSchedule"
nodeClassRef:
name: gpu-ec2-nodeclass
Then label your GPU workloads with a toleration. This keeps GPU costs contained — you control exactly how many expensive nodes can exist simultaneously. At SIVARO we run AI inference tasks in a gpu-pool capped at 16 vCPU and 64 GiB memory. That means at most two g4dn.4xlarge or four g4dn.xlarge nodes. It's an administrative cap, but it's saved us from a budget blowout when a CI pipeline accidentally asked for GPUs.
Using Consolidation Policies to Shrink Waste
Limits prevent expansion. Consolidation handles contraction. Karpenter v1.0 has two main consolidation policies: WhenUnderutilized and WhenEmpty. WhenUnderutilized aggressively bin-packs pods to fewer nodes, terminating ones that become less than 50% utilized (configurable via consolidationPolicy in the NodePool). WhenEmpty only consolidates completely empty nodes.
For cost control, always use WhenUnderutilized. Yes, it causes more node churn. Yes, it can disrupt long-running pod's if they aren't tolerant to re-scheduling. But the cost savings are substantial. We measured a 22% reduction in node count after switching from WhenEmpty to WhenUnderutilized on a general-purpose cluster.
Configuring consolidation with a cooldown:
yaml
consolidation:
enabled: true
policy: WhenUnderutilized
ttlSecondsAfterEmpty: 60 # wait 60 seconds before terminating empty node
ttlSecondsUntilExpired: 7200 # not relevant for cost, but for lifecycle
The ttlSecondsAfterEmpty is critical. Set it low (30–60 seconds) to reclaim capacity fast. If you set it to 300 seconds, you're paying for empty nodes for 5 minutes. Over a month that adds up. I've seen clusters with 30 empty nodes running for 10 minutes each because someone left the default TTL at 600 seconds. That's 5 node-hours of waste per event.
One more thing: consolidation only works if your pods are rescheduleable. If you have StatefulSets with local PVs, consolidation won't touch those nodes unless the PVCs are moved (which they can't be without manual intervention). For stateful workloads, use a separate NodePool with WhenEmpty and a longer TTL, or accept that those nodes are sticky.
Layering with HPA and VPA for Rightsizing
Karpenter limits are a backstop. They don't fix the problem of pods requesting too many resources. That's where horizontal pod autoscaling (HPA) and vertical pod autoscaling (VPA) come in. Kubernetes Rightsizing in 2026 explains the interplay well. The short version: if your pods request 4 vCPU but only use 1 vCPU, Karpenter will launch nodes sized for 4 vCPU. You pay for 3 wasted vCPUs per pod.
Set HPA to scale out instead of up. Set VPA (in recommendation mode) to adjust resource requests based on historical usage. Then Karpenter will see smaller requests and launch cheaper nodes. The combination is powerful: at a client we reduced cluster spend by 55% after implementing VPA recommendations and tightening instance-type limits.
But be careful with VPA in update mode. VPA can restart pods to apply new requests. That interacts badly with long-running batch jobs. We always run VPA in Off or Initial mode, then manually apply recommendations after review.
Monitoring and Alerting on Karpenter Behavior
"Measure or guess" — you know the saying. You need to monitor Karpenter's decisions. I use Kubernetes Cost Optimization: A 2026 Guide as a reference for tooling. Here's my setup:
- Kubecost — gives per-pod costs, node utilization, and Karpenter spending breakdowns. Cost allocation labels help identify which team or namespace triggered expensive node launches.
- Karpenter metrics — expose metrics like
karpenter_nodes_created,karpenter_nodes_terminated,karpenter_consolidation_durationvia Prometheus. I set alerts onkarpenter_nodes_createdper hour > 10 (potential drift). - AWS Cost Explorer — filter by tag
karpenter:trueto see node costs grouped by NodePool.
Set up a simple alert: if total node cost in the last 24 hours exceeds your budget threshold by 20%, page someone. At SIVARO we have a Slack bot that posts hourly summaries of NodePool utilization and pending pod counts. When a new instance family appears that we didn't allow, we get an alert.
Example Prometheus alert rule:
yaml
groups:
- name: karpenter-spend
rules:
- alert: KarpenterNodeSurge
expr: rate(karpenter_nodes_created[5m]) > 0.5
for: 10m
labels:
severity: warning
annotations:
summary: "Karpenter creating nodes at high rate ({{ $value }} per second)"
That alert catches configuration drift or a sudden workload spike. I've had cases where a team accidentally deployed a DaemonSet that requested huge resources, and Karpenter started launching nodes like a factory. The alert fired within 5 minutes.
Common Mistakes (and How I've Seen Companies Burn Money)
Mistake 1: No limits at all. The classic. You deploy Karpenter with a basic NodePool, no limits.resources, no instance-type restrictions. Then someone deploys a large batch job. $15,000 later you notice. Fix: always start with explicit instance-type lists and resource caps, even if you think you'll never need them.
Mistake 2: Using notIn instead of In for instance types. A common pattern is to exclude expensive families: node.kubernetes.io/instance-type NotIn [g4dn, p3, ...]. That's risky because AWS adds new families constantly. Karpenter will pick them if they fit. Use In with an allowed set instead. Yes, it's more maintenance when you need a new type. Yes, it's safer.
Mistake 3: Setting limits too low for burst workloads. At a gaming company, they set a NodePool limit of 32 vCPU for their game server fleet. During peak hours, pods couldn't schedule and users got connection errors. The solution: use multiple NodePools with different limits, and weight them. A "burst" pool with higher limits but tighter consolidation, and a "baseline" pool for steady load.
Mistake 4: Ignoring spot interruptions. Karpenter supports spot instances natively. But if you set limits assuming a certain number of spot nodes, and AWS reclaims them, Karpenter will launch On-Demand replacements — which cost 3x more. Use a separate NodePool for spot with its own limits, and set a hard cap on On-Demand spend. Cast AI's guide on Karpenter vs Cluster Autoscaler covers spot strategies in depth.
Mistake 5: Forgetting to clean up NodePools. When you change instance-type lists, old NodeClaims may persist. Karpenter doesn't automatically terminate nodes that no longer match the NodePool's requirements unless consolidation matches them. I've seen clusters running instances from deprecated families for weeks. Use kubectl delete nodeclaim -l node-lifecycle=karpenter to force re-evaluation after configuration changes.
FAQ
Q: Do I need to set limits on every NodePool I create?
A: Yes. Not setting limits is equivalent to giving Karpenter a blank check. Even if you think a NodePool is small, default behavior can surprise you. Start with conservative limits and adjust.
Q: What happens when a NodePool reaches its resource limit but pods are still pending?
A: Those pods will stay Pending with a FailedScheduling event from Karpenter. You need to monitor pending pod metrics and either increase limits or scale horizontally via additional NodePools.
Q: Can I set monetary budgets directly in Karpenter?
A: No, Karpenter doesn't have a native budget feature. You must translate budget dollars into resource limits (vCPU, memory). Use cost monitoring tools to map resource consumption to spend, then iterate. Third-party tools like Kubernetes Cost Optimization Tools for 2026 can help.
Q: How do I handle workloads that genuinely need large instances (e.g., ML training)?
A: Create a dedicated NodePool with higher limits, limited instance types, and strict tolerations. Apply limits.resources per-pool to cap concurrent large nodes. Combine with spot instances if possible. See Smarter Cost Optimization with Karpenter for a migration approach.
Q: Should I use memory limits in NodePool or rely on instance-type restrictions?
A: Both. Instance-type restrictions prevent wildly expensive families; resource limits cap total cluster spend. They work together. I always set both.
Q: Does Karpenter support using tags to track costs?
A: Yes, EC2NodeClass has a tags field. Tag all nodes with Karpenter:true and a pool identifier. AWS Cost Explorer lets you filter by these tags for granular cost allocation.
Q: What's the best consolidation policy for cost control?
A: WhenUnderutilized with a short empty-node TTL (30-60 seconds). It maximizes packing efficiency. For stateful workloads, use WhenEmpty or disable consolidation.
Conclusion
Learning how to set Karpenter limits to control spending isn't optional — it's table stakes for running Kubernetes on AWS in 2026. The tool has evolved rapidly, but the core discipline remains: restrict instance types, cap resource pools, enforce consolidation, and monitor relentlessly. Start your Karpenter deployment with the smallest possible allowed set of instance types and a conservative resource limit. You can always expand later. The opposite — cleaning up a spend explosion — is far harder.
At SIVARO, we follow this playbook for every client deployment. The results are consistent: 30–50% savings on compute costs just from proper configuration, before any workload right-sizing. Combine Karpenter limits with HPA/VPA optimization and you'll have a cluster that scales intelligently without bleeding cash.
The cloud doesn't have to be a cost center. You just have to tell Karpenter where the fence is.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.