Back to blog

Karpenter: how to cut Kubernetes costs without sacrificing performance

Introduction

One of the biggest challenges in Kubernetes environments isn't just scaling applications, but making sure infrastructure grows and shrinks efficiently.

In a traditional Kubernetes cluster running on AWS, it's common to find an architecture built on Amazon EKS (Elastic Kubernetes Service) with Managed Node Groups or Auto Scaling Groups (ASGs).

The problem shows up when application demand varies.

Imagine a cluster with:

  • APIs;
  • workers;
  • RabbitMQ or Kafka consumers;
  • CI/CD pipelines;
  • internal applications;
  • processing workloads;
  • observability components.

During business hours, the cluster might need 20 nodes. Overnight, maybe only six.

If the Node Groups are sized for peak load, a large part of the infrastructure will sit idle for many hours.

A traditional approach would be to use:

Pods
 ↓
Horizontal Pod Autoscaler
 ↓
Pending Pods
 ↓
Cluster Autoscaler
 ↓
Auto Scaling Group
 ↓
EC2

The Cluster Autoscaler solves part of the problem, but works mainly by adjusting previously defined node groups.

Karpenter takes a different approach.

Instead of asking:

"Which Node Group should grow?"

it works closer to:

"What resources do these pods actually need, and what infrastructure capacity meets that demand?"

From there, Karpenter can pick different EC2 families, sizes, zones and capacity models.

That flexibility lets you combine:

  • Spot Instances;
  • On-Demand;
  • multiple EC2 types;
  • node consolidation;
  • pod-driven scalability;
  • workloads with different requirements.

The result can be significantly more efficient infrastructure without necessarily hurting application performance.


What Karpenter is

Karpenter is an open source Kubernetes node autoscaling project originally created by AWS.

Its goal is to dynamically provision compute infrastructure based on workload requirements.

Instead of relying exclusively on fixed instance groups, Karpenter watches pods that can't be scheduled and figures out what infrastructure can run them.

Simplifying:

Deployment
      ↓
Pods
      ↓
Kubernetes Scheduler
      ↓
Pending Pods
      ↓
Karpenter
      ↓
Requirement analysis
      ↓
EC2 Fleet / AWS APIs
      ↓
New EC2
      ↓
Node joins the cluster
      ↓
Pods get scheduled

Karpenter considers factors like:

  • CPU;
  • memory;
  • architecture;
  • availability zone;
  • affinity;
  • anti-affinity;
  • taints;
  • tolerations;
  • topology spread;
  • instance type;
  • Spot capacity;
  • On-Demand capacity.

In current releases, configuration mainly revolves around objects like NodePool, NodeClaim, and, on the AWS provider, EC2NodeClass.


Key concepts

NodePool

The NodePool determines which types of nodes Karpenter can create.

It defines constraints such as:

architecture
operating system
EC2 family
size
zone
capacity model
taints
labels
limits
disruption policies

A cluster can have several NodePools.

For example:

NodePool: applications
  → Spot
  → c/m/r
  → amd64

NodePool: databases
  → On-Demand
  → r-family
  → amd64

NodePool: monitoring
  → Spot + On-Demand
  → m/r

NodePool: cicd
  → Spot
  → c/m

Separating workloads this way lets you control cost and availability individually.

The documentation recommends NodePools be, when possible, mutually exclusive, reducing ambiguity about which pool should serve a given workload.


EC2NodeClass

The EC2NodeClass holds AWS-specific configuration.

For example:

  • AMI;
  • subnets;
  • Security Groups;
  • IAM Role;
  • EBS volumes;
  • kubelet configuration;
  • EC2 tags.

While NodePool answers:

"What type of capacity can I use?"

EC2NodeClass answers:

"How should this instance be created in AWS?"

The same EC2NodeClass can be used by multiple NodePools.


NodeClaim

The NodeClaim represents a concrete capacity request created by Karpenter.

We can visualize it like this:

NodePool
   ↓
allowed template
   ↓
Karpenter detects demand
   ↓
NodeClaim
   ↓
EC2
   ↓
Kubernetes Node

The NodeClaim contains the requirements resulting from combining:

  • NodePool;
  • pod requirements;
  • required resources;
  • cloud provider availability.

It's usually not an object the operator creates manually.


How Karpenter works

Imagine three pending pods:

api-1      500m CPU   512Mi
api-2      500m CPU   512Mi
worker-1   2000m CPU  2Gi

Karpenter notices there isn't enough capacity in the cluster.

It analyzes:

total CPU
total memory
nodeSelector
affinity
topologySpreadConstraints
taints/tolerations
architecture
zones
capacity-type

Then it looks for EC2 offerings capable of running these pods.

Unlike a strategy that determines in advance only:

t3.xlarge

it's possible to provide flexibility to use:

c6a.large
c6a.xlarge
c7a.large
m6a.large
m6a.xlarge
m7a.large
t3a.xlarge
...

as long as those instances satisfy the defined requirements.

This flexibility particularly matters for Spot.

The more unnecessary restrictions you apply, the smaller the available capacity pool becomes. AWS recommends avoiding overly restrictive instance-type constraints for workloads that can be flexible.


Where the savings actually come from

Karpenter alone doesn't make a cluster cheap.

The cost reduction usually comes from combining several strategies:

Karpenter
    +
Spot
    +
flexible instances
    +
correct requests
    +
consolidation
    +
pod autoscaling
    +
removing idle capacity

Before

Imagine five nodes:

5 × m6i.xlarge

Each has roughly:

4 vCPU
16 GiB RAM

Capacity:

20 vCPU
80 GiB RAM

But the workloads are using roughly:

8 vCPU
32 GiB RAM

There's a lot of idle infrastructure.

After

With Karpenter, pods can be reorganized and underutilized nodes can become candidates for consolidation.

The cluster might end up looking like, for example:

2 × m6a.xlarge
1 × c6a.xlarge

or another combination that satisfies the scheduling requirements.

The actual choice will depend on:

  • price;
  • availability;
  • requests;
  • architecture;
  • constraints;
  • capacity model.

The goal shouldn't be to manually pick the cheapest EC2.

The goal is to give enough flexibility for Karpenter to choose efficient capacity.


Full example: EC2NodeClass

A near-production example:

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: production
spec:
  amiSelectorTerms:
    - alias: al2023@v20250808

  role: KarpenterNodeRole-production

  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: production

  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: production

  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 80Gi
        volumeType: gp3
        encrypted: true
        deleteOnTermination: true

  tags:
    Environment: production
    ManagedBy: karpenter

What this configuration does

The EC2NodeClass defines how nodes are created in AWS.

amiSelectorTerms:

determines the AMI used.

In production, it's advisable to pin a tested AMI version instead of automatically using @latest, preventing a new image from entering the cluster without prior validation. This recommendation is explicitly made by the EKS best-practices documentation for Karpenter.

role:

determines the IAM Role used by the instances.

subnetSelectorTerms:

lets you discover subnets through tags.

securityGroupSelectorTerms:

selects the Security Groups used by the nodes.

volumeType: gp3

uses GP3 EBS volumes.

And:

encrypted: true

ensures volume encryption.

The role name, AMI version, disk size, tags, subnets and Security Groups should be adapted to your environment.


Spot NodePool for applications

Now we can create a NodePool.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: prod-app
spec:
  template:
    metadata:
      labels:
        workload-type: application

    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: production

      taints:
        - key: prod-app
          value: "true"
          effect: NoSchedule

      requirements:
        - key: kubernetes.io/arch
          operator: In
          values:
            - amd64

        - key: kubernetes.io/os
          operator: In
          values:
            - linux

        - key: karpenter.sh/capacity-type
          operator: In
          values:
            - spot

        - key: karpenter.k8s.aws/instance-category
          operator: In
          values:
            - c
            - m

        - key: karpenter.k8s.aws/instance-generation
          operator: Gte
          values:
            - "6"

      expireAfter: 720h
      terminationGracePeriod: 2m

  limits:
    cpu: "200"
    memory: 400Gi

  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 5m

    budgets:
      - nodes: "20%"

This NodePool lets Karpenter choose instances:

c*
m*

from generation six onward, as long as they meet the requirements.

Instead of defining:

values:
  - m6a.xlarge

we offer several possibilities.

This improves the odds of finding available Spot capacity.

Karpenter supports constraints by architecture, operating system, capacity type, category, generation and other instance characteristics.

Taint

The NodePool creates:

prod-app=true:NoSchedule

As a result, only workloads with the matching toleration can run on these nodes.

This keeps random cluster components off capacity meant for applications.

Limits

limits:
  cpu: "200"
  memory: 400Gi

sets a growth cap on the pool.

Without spec.limits, maximum growth could end up bounded mostly by the cloud provider's quotas, so setting limits is an important operational and financial safeguard.


Deployment using the NodePool

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api
  namespace: production
spec:
  replicas: 4

  selector:
    matchLabels:
      app: orders-api

  template:
    metadata:
      labels:
        app: orders-api

    spec:
      terminationGracePeriodSeconds: 30

      tolerations:
        - key: prod-app
          operator: Equal
          value: "true"
          effect: NoSchedule

      nodeSelector:
        workload-type: application

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: orders-api

      containers:
        - name: api
          image: registry.example.com/orders-api:2.8.1

          ports:
            - containerPort: 8080

          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi

          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10

          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20

There's a fundamental point here:

resources:
  requests:
    cpu: 500m
    memory: 512Mi

Karpenter uses requests to understand how much resource the pods need.

If requests are wildly oversized, Karpenter will conclude it needs more infrastructure than is actually necessary.

For example:

Actual CPU usage:      150m
CPU request:       2000m

For Kubernetes, that pod reserves:

2 CPUs

even though it only uses a small fraction of that.

So FinOps in Kubernetes starts with correctly sizing workloads.


PodDisruptionBudget

If we use consolidation, we need to think about availability.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: orders-api
  namespace: production
spec:
  minAvailable: 3

  selector:
    matchLabels:
      app: orders-api

With four replicas:

replicas = 4
minAvailable = 3

a voluntary disruption must preserve at least three available pods.

The PodDisruptionBudget (PDB) helps protect the application during operations like drain and consolidation.

But excessively restrictive PDBs can also prevent Karpenter from consolidating nodes. The documentation itself shows PDBs as a possible cause of nodes marked Unconsolidatable.

So:

too weak a PDB
→ availability risk

too restrictive a PDB
→ infrastructure that's hard to consolidate

correct PDB
→ balance between availability and efficiency

A day-to-day example

Consider a SaaS platform with:

EKS

├── Node.js APIs
├── Go APIs
├── workers
├── RabbitMQ
├── Redis
├── PostgreSQL
├── Prometheus
├── Grafana
└── CI/CD pipelines

During business hours:

CPU requested: ~45 vCPU

At night:

CPU requested: ~15 vCPU

Before Karpenter, the company keeps:

12 × m6a.xlarge

running 24 hours a day.

The cluster needs that capacity during peaks but keeps a big chunk of it idle for many hours.


Applying Karpenter

The architecture moves to:

                    Kubernetes
                         │
             ┌───────────┴───────────┐
             │                       │
         APIs/Workers          Critical infra
             │                       │
             ▼                       ▼
      NodePool prod-app       NodePool critical
             │                       │
           Spot                  On-Demand
             │                       │
        c/m families              m/r family

So:

APIs and workers

Use:

Spot

because they're:

  • replicated;
  • stateless;
  • interruption-tolerant;
  • fast to start.

Databases or extremely sensitive components

Can stay:

On-Demand

or on dedicated Managed Node Groups.

The goal isn't to run 100% of the cluster on Spot.

It's to use Spot wherever the architecture can tolerate interruptions.


Spot doesn't just mean cheap infrastructure

Spot has an important characteristic:

capacity can be interrupted.

On AWS, certain Spot interruptions give roughly two minutes of notice. Karpenter has built-in handling for these events and can start a replacement while cordoning/draining the affected node.

For that, it's important to configure interruption handling, including the SQS queue Karpenter uses.

Architecturally:

AWS interruption event
         ↓
EventBridge
         ↓
SQS
         ↓
Karpenter
         ↓
cordon / taint / drain
         ↓
new NodeClaim
         ↓
new EC2

Spot should mainly be used for workloads built to disappear and come back.


Spot + On-Demand strategy

An alternative is to allow both models:

- key: karpenter.sh/capacity-type
  operator: In
  values:
    - spot
    - on-demand

In NodePools that support multiple capacity models, Karpenter can fall back to alternatives based on availability. The current documentation also covers reserved capacity for specific capacity reservations; this shouldn't be confused with traditional Reserved Instances.

Another, more controlled strategy is to have independent NodePools:

prod-app-spot
prod-app-ondemand

and use:

  • labels;
  • weights;
  • affinities;
  • topology spread.

The choice depends on the organization's reliability policy.


Consolidation

One of Karpenter's most important features for FinOps is consolidation.

Imagine:

Node A → 20% utilized
Node B → 25% utilized
Node C → 20% utilized

It might be possible to move the workloads to:

Node D → 70% utilized

and remove the three previous ones.

Karpenter can evaluate:

Node A ──┐
Node B ──┼──► more efficient node
Node C ──┘

The documentation describes consolidation through removal or replacement operations, evaluating whether workloads can run on existing capacity or on a more efficient alternative.


consolidateAfter

Consolidation doesn't need to happen immediately.

For example:

consolidateAfter: 5m

means Karpenter waits for a period of stability before considering certain consolidations.

This is useful for workloads with variable behavior.

Imagine:

10:00 → scales up
10:02 → scales down
10:04 → scales up
10:06 → scales down

Overly aggressive consolidation can produce:

creates node
↓
removes node
↓
creates node
↓
removes node

This behavior is informally known as node churn.

A well-tuned consolidateAfter reduces unnecessary node movement.


Disruption Budgets

An important safeguard for production is controlling how many nodes Karpenter can interrupt at the same time.

Example:

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized
  consolidateAfter: 5m

  budgets:
    - nodes: "10%"

This way, Karpenter doesn't indiscriminately run large numbers of simultaneous voluntary interruptions.

NodePool Disruption Budgets limit voluntary actions related to mechanisms like consolidation and drift.

This safeguard matters especially when there are dozens or hundreds of nodes.


Production: separating workloads

A mature architecture might have:

                    EKS
                     │
     ┌───────────────┼────────────────┐
     │               │                │
prod-app         monitoring         database
     │               │                │
Spot            Spot/OnDemand      On-Demand
     │               │                │
c/m               m/r                r

And also:

ci-cd
  │
Spot
  │
c/m

prod-app

Applications:

Node.js
Go
PHP
Java

Can use Spot when they have:

  • multiple replicas;
  • readinessProbe;
  • PDB;
  • proper shutdown;
  • distribution across AZs.

CI/CD

Build agents are great Spot candidates when jobs can be retried.

Observability

Can use a hybrid strategy.

Databases

Stateful workloads require more careful analysis.

Karpenter can run stateful workloads, but that doesn't automatically mean a critical database should be placed on Spot capacity.


FinOps: requests are fundamental

Consider:

resources:
  requests:
    cpu: "4"
    memory: 8Gi

But observability shows:

CPU P95: 700m
RAM P95: 2.5Gi

If there are ten pods:

Kubernetes sees:

40 vCPU
80 GiB

even though actual consumption is far below that.

Karpenter will provision capacity based on what the scheduler needs to reserve.

So the correct optimization sequence is usually:

Metrics
   ↓
Rightsizing
   ↓
Correct requests
   ↓
Karpenter
   ↓
Consolidation
   ↓
Spot

Not:

wrong requests
   ↓
Karpenter
   ↓
"why didn't it save money?"

Observability

Karpenter needs to be monitored like any critical platform component.

It exposes Prometheus-format metrics related to provisioning, NodeClaims, the cloud provider, reconciliations and consolidation.

A typical stack:

Karpenter
    │
    ├── metrics ──► Prometheus ──► Grafana
    │
    └── logs ─────► Loki ────────► Grafana

Important points to track include:

time to create a node
pending NodeClaims
cloud provider errors
provisioning failures
NotReady nodes
Pending pods
Spot vs. On-Demand ratio
consolidations
interruptions
cost per NodePool
CPU/memory utilization
requests vs. utilization

Some available metrics include data on cloud provider errors and operation durations, plus consolidation-related metrics.


Logs

Controller logs can be inspected with:

kubectl logs \
  -n kube-system \
  -l app.kubernetes.io/name=karpenter \
  --tail=200

To follow in real time:

kubectl logs \
  -n kube-system \
  -l app.kubernetes.io/name=karpenter \
  -f

In Loki, you can centralize these logs and build dashboards or alerts for events like:

failed scheduling
insufficient capacity
NodeClaim failed
cloud provider error
unconsolidatable

Troubleshooting

Pods stay Pending

First:

kubectl get pods -A --field-selector=status.phase=Pending

Then:

kubectl describe pod <pod>

Check the:

Events

section.

Common errors:

Insufficient cpu
Insufficient memory
untolerated taint
node affinity conflict
topology constraints

Checking NodePools

kubectl get nodepool

Details:

kubectl describe nodepool prod-app

Check:

requirements
limits
conditions
disruption

Checking NodeClaims

kubectl get nodeclaims

Then:

kubectl describe nodeclaim <nodeclaim>

This helps identify issues between:

Pod
↓
NodePool
↓
NodeClaim
↓
EC2

Checking EC2NodeClass

kubectl get ec2nodeclass

And:

kubectl describe ec2nodeclass production

Problems can include:

subnet not found
wrong security group
invalid AMI
insufficient IAM
capacity unavailable

Kubernetes events

One of the most useful commands:

kubectl get events -A \
  --sort-by=.metadata.creationTimestamp

Also:

kubectl get nodes \
  -L karpenter.sh/nodepool,karpenter.sh/capacity-type,node.kubernetes.io/instance-type

This lets you see something like:

NODE       NODEPOOL    CAPACITY     INSTANCE
node-01    prod-app    spot         c6a.xlarge
node-02    prod-app    spot         m6a.large
node-03    critical    on-demand    m6i.xlarge

Security

IAM

Karpenter needs to create and remove AWS infrastructure.

So IAM permissions need to follow the principle of least privilege.

Avoid policies like:

{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}

The controller's and the instances' permissions should have clearly separated responsibilities.


ServiceAccounts

On EKS, prefer workload-specific identity mechanisms, like EKS Pod Identity or IRSA depending on the adopted architecture and version.

Avoid hardcoded AWS credentials in:

env:
  AWS_ACCESS_KEY_ID: ...

or:

Secret:
  access-key: ...

when federated identity can be used instead.


Workload isolation

Use:

taints
tolerations
labels
nodeAffinity
NetworkPolicies
namespaces
RBAC

to prevent workloads from running where they shouldn't.

For example:

database → database nodes
monitoring → monitoring nodes
applications → prod-app nodes
CI/CD → ci-cd nodes

Karpenter doesn't replace Kubernetes isolation policies.


Availability

Savings shouldn't come at the cost of availability.

An application on Spot should account for:

Deployment
   │
   ├── replica 1 → AZ A
   ├── replica 2 → AZ B
   └── replica 3 → AZ A/B

Use:

topologySpreadConstraints:

or equivalent mechanisms to avoid unnecessary concentration of replicas.

Also consider:

PDB
readinessProbe
livenessProbe
startupProbe
terminationGracePeriodSeconds
graceful shutdown

What to avoid

Pinning a single instance type

Avoid:

- key: node.kubernetes.io/instance-type
  operator: In
  values:
    - m6a.xlarge

without a real need for it.

This considerably reduces flexibility.

It's better to work with requirements:

- key: karpenter.k8s.aws/instance-category
  operator: In
  values:
    - c
    - m

when the workload allows it.


Putting everything on Spot

Don't do:

Entire Kubernetes cluster
      ↓
100% Spot

just to maximize savings.

Classify workloads by interruption tolerance.

For example:

WorkloadPossible strategy
Stateless APISpot
Idempotent workerSpot
CI/CD AgentSpot
Batch processingSpot
Observabilityhybrid
Critical databaseOn-Demand
Essential componentsOn-Demand or hybrid strategy

These are architectural decisions, not absolute rules.


Don't ignore requests

Avoid:

resources: {}

In autoscaling clusters, this hurts:

  • scheduling;
  • capacity;
  • bin packing;
  • predictability;
  • FinOps.

Don't indiscriminately use the @latest AMI in production

Example:

amiSelectorTerms:
  - alias: al2023@latest

is convenient in test environments.

In production, an unexpected update can introduce changes to:

kernel
container runtime
kubelet
drivers
configuration

AWS's recommendation is to use known, production-tested AMIs and validate new versions in non-production environments before rolling out an update.


Development vs. production

Development

Might be acceptable:

100% Spot
aggressive consolidation
few replicas
generic NodePools
shorter expireAfter

The main goal might be cost.


Production

An appropriate strategy tends to include:

Spot + On-Demand
multiple AZs
PDB
topology spread
interruption handling
pinned AMIs
observability
NodePool limits
disruption budgets
reviewed requests
per-workload isolation

Production should prioritize balancing:

cost
+
resilience
+
performance
+
predictability

Sample production architecture

                          AWS
                           │
                          EKS
                           │
              ┌────────────┼────────────┐
              │            │            │
             API        Workers      Database
              │            │            │
            HPA           KEDA        Stateful
              │            │            │
              └──────┬─────┘            │
                     │                  │
                     ▼                  ▼
               Pending Pods      Critical Pods
                     │                  │
                     ▼                  ▼
                 Karpenter          NodePool
                     │              On-Demand
          ┌──────────┴──────────┐
          │                     │
        Spot                 On-Demand
          │                     │
      c/m families           m/r families

In this model:

HPA

Scales APIs by metrics.

CPU
memory
custom metrics

KEDA

Can scale workers based on:

RabbitMQ
Kafka
SQS
Prometheus

Karpenter

Scales infrastructure when pods no longer fit existing nodes.

So:

HPA/KEDA
    ↓
more Pods
    ↓
Pending Pods
    ↓
Karpenter
    ↓
more Nodes

And on the way down:

demand drops
    ↓
HPA/KEDA reduces Pods
    ↓
nodes become underutilized
    ↓
Karpenter consolidates
    ↓
EC2 removed

This interaction is one of the most efficient forms of Kubernetes elasticity.


A complete FinOps strategy

Karpenter should be part of a bigger strategy.

A mature architecture might use:

Prometheus
   ↓
usage analysis
   ↓
VPA recommendations / rightsizing
   ↓
correct requests
   ↓
HPA/KEDA
   ↓
Karpenter
   ↓
Spot
   ↓
consolidation
   ↓
OpenCost

OpenCost can complement the environment by showing costs by:

namespace
deployment
pod
label
Node

That way you can uncover situations like:

Namespace A
CPU requests: 30 cores
Actual CPU: 6 cores

In that case, switching On-Demand for Spot might cut cost, but the structural problem is still there:

oversized requests.

Efficient FinOps starts with demand, not just infrastructure price.


When to use Karpenter

Karpenter is especially interesting when there are:

  • EKS clusters with variable demand;
  • many workloads;
  • stateless applications;
  • frequent autoscaling;
  • Spot usage;
  • a need for multiple EC2 types;
  • workloads with different requirements;
  • significant cost concerns;
  • unpredictable growth;
  • environments using HPA or KEDA.

The bigger the demand swings, the bigger the potential benefit tends to be.


When Karpenter might not be necessary

Not every cluster needs Karpenter.

Imagine:

3 nodes
3 applications
nearly constant load

If the environment runs continuously on:

3 × m6i.large

and rarely changes, a Managed Node Group might be enough.

Adding:

Karpenter
NodePools
NodeClaims
EC2NodeClasses
interruption handling
disruption policies
monitoring

increases operational complexity.

In that scenario, the savings might not justify the complexity.

Karpenter tends to bring the most value to dynamic environments.


Checklist before using Karpenter in production

Before migrating, validate:

  • reviewed requests and limits;
  • replicated applications;
  • readiness and liveness probes configured;
  • graceful shutdown;
  • PodDisruptionBudgets;
  • distribution across AZs;
  • NodePools separated by responsibility;
  • CPU and memory limits;
  • Spot/On-Demand strategy;
  • interruption handling;
  • IAM following least privilege;
  • encrypted disks;
  • controlled AMIs;
  • Prometheus metrics;
  • Grafana dashboards;
  • centralized logs;
  • alerts for provisioning failures;
  • real node-interruption tests.

Practical summary

Karpenter changes the traditional logic of Kubernetes infrastructure autoscaling.

Instead of thinking only:

I need to grow my t3.xlarge group

we can think:

I have these pods
with these requirements
and I need the most suitable capacity available

That difference lets you take better advantage of:

Spot
+
multiple instance types
+
multiple families
+
consolidation
+
dynamic autoscaling

The end-to-end flow can be represented as:

Demand increases
     ↓
HPA / KEDA
     ↓
New Pods
     ↓
Pending Pods
     ↓
Karpenter
     ↓
NodeClaim
     ↓
Spot / On-Demand EC2
     ↓
Kubernetes Node
     ↓
Pods running

When demand drops:

fewer Pods
     ↓
underutilized nodes
     ↓
consolidation
     ↓
drain
     ↓
EC2 termination
     ↓
cost reduction

Conclusion

Karpenter shouldn't be treated as just a tool that automatically creates EC2 instances.

Its biggest benefit shows up when it's part of a Kubernetes architecture designed for elasticity.

Cost reduction comes from combining:

rightsizing
+
application autoscaling
+
dynamic provisioning
+
Spot
+
instance-type flexibility
+
consolidation
+
observability

A cluster with oversized requests, applications that can't tolerate interruptions, and overly restrictive NodePools will keep being inefficient even with Karpenter.

On the other hand, when applications are correctly sized and architected for resilience, Karpenter lets infrastructure track real demand much more closely.

For Platform Engineering, this means offering an infrastructure layer capable of providing different capacity profiles:

applications → Spot
critical     → On-Demand
batch        → Spot
ci-cd        → Spot
database     → stable capacity

For SRE, it means combining elasticity with availability, observability and disruption controls.

For FinOps, it means shrinking the gap between provisioned capacity and actually needed capacity.

And that's the core point of Karpenter: not cutting costs simply by picking cheaper machines, but letting Kubernetes use the right amount and type of infrastructure for each moment, while keeping performance, availability and reliability as architectural requirements.

Want to apply this to your environment?

Book a free 30-minute call. I will look at your setup and tell you where to start.

Book a call

Get new articles by email

One email when a new article on infrastructure, backend and AI is published. No spam, unsubscribe anytime.