← Projects
Cost & Reliability

Five Levers to a 70% Lower Cloud Bill

Karpenter-driven, ~80% spot, with weighted NodePools and on-demand fallbacks: 60–70% lower cloud cost without trading away reliability.

60–70%
cloud cost reduction vs all on-demand
~80% spot
of the fleet
$130,200/yr
saved on OpenCTI alone
99.9%
SLA target held

Running a 300–400-node platform mostly on spot capacity sounds reckless until you engineer for it. Everything on-demand would have cost roughly 3× more per environment, so the whole discipline is about earning back that 3× without trading away a 99.9% SLA. It comes down to five levers that compound: put the right workloads on spot, delete the waste spot leaves behind, scale down as aggressively as you scale up, measure spend per workload, and drag legacy stacks onto Kubernetes so they can benefit from all four. This is how each one works.

Lever 1: Placement that matches risk to workload

The first and biggest saving is simply deciding what is allowed to run on spot. A spot node can be reclaimed with two minutes' notice, so the question for every tier is blunt: if this vanishes mid-flight, do we lose money or just a little time?

In Dev, almost everything answers "just time." AI, application, and tooling workloads all run on spot; only databases stay on-demand. Prod is barely different: AI, tooling, and the bulk of the application tier still ride spot, and the database tier is the only one held fully on-demand. The one refinement is that applications flagged critical: true don't move wholesale off spot. They pin a percentage of their pods to on-demand as a floor, using Karpenter's capacity-spread ratio split, so even the critical apps stay majority-spot.

This isn't an exotic bet. Stateless pools on spot with an automatic on-demand fallback is a well-worn pattern, and when the instance-type spread is wide enough the fallback almost never has to fire. Our own ~80%-spot fleet is where most of the ~70% saving comes from; the rest of this page is about protecting and extending it.

  • Interruption-tolerant tiers (accelerated AI training/batch, stateless apps, platform tooling) ride spot in both environments
  • In Prod, only apps flagged critical: true pin a fraction of pods to on-demand (via the ratio split); the majority still run on spot
  • Stateful databases are the one tier never on spot: a reclaim mid-write is a data risk no discount justifies
  • A native SQS interruption queue catches the 2-minute reclaim notice so Karpenter drains and replaces the node before the workload notices
Match the capacity type to the workload's risk

Spot where an interruption is survivable, on-demand where it is not. Toggle the environment.

3 of 4 tiers ride spot
AI / acceleratedspot· Prod

Still spot: the accelerated fleet is the single biggest line item, and the work is interruptible.

Why it matters: everything on-demand would cost roughly 3× more per environment. Putting the interruption-tolerant tiers on spot is where about 70% of the savings live.
The same fleet carries two risk postures: toggle Dev and Prod to see which tiers stay on spot and which pull back to on-demand once customer traffic is on the line.

Lever 1, continued: A weighted ladder of NodePools

Placement is expressed as a set of Karpenter NodePools with different weights. Weight is a tie-breaker: when a pod matches more than one pool, Karpenter tries the higher-weight pool first, though it will fall through to a lower-weight pool rather than leave a pod pending. Databases sit at the top so stateful pods bind to their on-demand pool first; the application and tooling pools sit lower, spot-first with a deliberately wide instance-type spread so there is always deep spot capacity to land on.

Within a pool that allows both capacity types, Karpenter's price-capacity-optimized allocation picks the cheapest available offering, almost always spot, and only reaches for on-demand when spot runs dry. When we want a hard on-demand floor for a specific app rather than "whatever's cheapest," the on-demand/spot ratio split does it: two NodePools share a synthetic capacity-spread label (spot gets four values, on-demand gets one) and a pod topology spread over that label lands the workload roughly 4:1 on spot. That's the mechanism behind critical: true staying majority-spot: a floor, not a wholesale move. (The ladder and weights below are representative, illustrating the pattern rather than dumping production config.)

yaml
# Representative pattern illustrating the on-demand/spot ratio split,
# not a literal dump of production config.

# Spot pool: 4 capacity-spread values -> 4 of every 5 virtual domains.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: apps-spot
spec:
  weight: 20
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
        - key: capacity-spread
          operator: In
          values: ["2", "3", "4", "5"]
---
# On-demand pool: 1 capacity-spread value -> the remaining 1 in 5.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: apps-on-demand
spec:
  weight: 20
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: capacity-spread
          operator: In
          values: ["1"]
---
# Only critical-flagged pods spread across the domains, landing ~4:1 on
# spot. Everything else just takes the cheapest capacity, almost always spot.
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: capacity-spread
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              critical: "true"
A weighted ladder of NodePools

Weight breaks ties: when a pod fits more than one pool, Karpenter prefers the higher weight. Each pool declares which capacity types it will accept.

Applicationweight 20spoton-demand
Instances
c · m · r, ≥ 8 GB, no accelerators
Taint
none
Runs
stateless services and APIs

Spot-first with a wide instance-type spread. Apps flagged critical: true don't leave spot. They pin a share of pods to on-demand as a floor, using the capacity-spread ratio split (≈ 4:1, so ~80% still land on spot).

Spot-first, not spot-only:pools that allow both capacity types let Karpenter's price-capacity-optimized allocation pick the cheapest available offering, almost always spot, and reach for on-demand only when spot capacity runs dry. Where we want a guaranteed on-demand floor, a topology-spread ratio split over a capacity-spread label pins the mix.
Click through the ladder: weight sets the order pools are tried, and each pool declares which capacity types it will accept.

Lever 2: Consolidation keeps the cheap fleet cheap

Provisioning cheap nodes is only half the job; the other half is relentlessly removing the ones you no longer need. Karpenter's consolidation is the single biggest ongoing lever after spot itself. It continuously deletes empty nodes, and (under WhenEmptyOrUnderutilized) simulates removing a node and rescheduling its pods, replacing two half-full nodes with one better-packed (often cheaper) instance.

The part people miss is that consolidation isn't only on-demand → spot. Spot-to-spot consolidation will replace a spot node with a cheaper spot instance type without ever leaving spot, but only when the NodePool offers at least fifteen instance types. That floor exists to stop a "race to the bottom," where Karpenter keeps swapping onto the single cheapest, least-stable instance and pays it all back in churn. It's also why the allocation strategy is price-capacity-optimized, not lowest-price: it trades a sliver of discount for markedly lower interruption rates.

Consolidation keeps the cheap fleet cheap

Provisioning spot nodes is half the job. The other half is relentlessly removing waste. Pick a disruption scenario.

Two half-full nodes could be oneWhenEmptyOrUnderutilized
2 nodes · ~45% each1 node · ~90%

Karpenter simulates removing a node and rescheduling its pods elsewhere. If everything still fits, it replaces the pair with a single, better-packed node, often a different, cheaper instance type.

Guardrail: Respects PodDisruptionBudgets and do-not-disrupt annotations, so stateful pods aren't yanked.
Three disruption scenarios Karpenter handles automatically (empty, underutilized, and spot-to-spot), each with the guardrail that keeps it safe.

Lever 3: Scaling down is a cost strategy

Scalability usually gets sold as the ability to absorb load. The cost win is the opposite direction: giving compute back the moment it isn't in use. Three autoscalers do this on three different axes, and none of them is the cluster autoscaler that adds and removes nodes. These shape what runs on the nodes we already pay for.

The Vertical Pod Autoscaler right-sizes CPU and memory requests so we stop reserving capacity nothing uses; its newer InPlace and InPlaceOrRecreate modes lean on Kubernetes in-place pod resize to tighten a running pod without the eviction churn that used to make VPA risky on stateful workloads. The Cluster Proportional Autoscaler scales cluster-shaped add-ons like CoreDNS to the node and core count, so they aren't pinned at peak on a fleet that breathes between 300 and 400 nodes. And KEDA takes event-driven workers where the native HPA can't, all the way to zero, waking them straight off queue depth when work arrives.

Three autoscalers, three different axes

The cluster autoscaler adds and removes nodes. These shape what runs on them, so the nodes we do pay for stay busy.

Requests that match reality, not guesses· right-sizes each pod
Offrecommends only: you read the numbers, nothing moves
Initialsets requests at admission, leaves running pods alone
Recreateevicts and reschedules to apply new requests
InPlaceresizes a running pod's requests without evicting it (K8s 1.33+)

Over-requested CPU and memory is invisible waste: it reserves capacity nothing uses, so nodes fill up on paper long before they fill up in practice. InPlace resize means we can tighten requests without the eviction churn that used to make VPA scary on stateful pods.

VPA (right-size), CPA (scale with cluster size), and KEDA (scale to zero on events), three axes of scaling down that the native HPA alone can't cover.

Lever 4: Turn the bill into a per-workload signal

A monthly AWS invoice is a single number; it can't tell you which team, service, or one careless resource request is driving it. OpenCost (the CNCF's vendor-neutral cost-allocation spec) rebuilds that number from the inside. It reads Prometheus and cAdvisor, prices every node's CPU, memory, GPU, disk, network, and load balancers from cloud or custom rates, and splits each asset's cost into three buckets: the workloads that actually used it, the idle capacity nobody used, and cluster overhead.

The allocation model has one detail that changes behaviour: a workload is billed for max(request, usage). Reserve four cores and use one, and OpenCost charges you for four, and books the other three as idle cost against your namespace, not the cluster's. That makes waste impossible to hide. The /allocation API becomes a worklist: aggregate the same spend by namespace, controller, pod, label, or node, flip on shareIdle to see who's really carrying the slack, and the biggest over-request is the next thing VPA should tighten.

It isn't only in-cluster, either. The Cloud Costs integration reads the AWS Cost and Usage Report, so the actual amortized figures (spot discounts included) land in the same view as the list-price estimates. That's what closes the loop: placement, consolidation, and scaling all become measurable, and the ~80%-spot ratio itself turns into a number we watch per node pool rather than one we assume.

  • max(request, usage) billing surfaces over-provisioning as named idle cost, straight into the VPA worklist
  • One spend, sliced any way: namespace, controller, pod, label, or node; shareIdle attributes the slack
  • Cloud Costs reads the AWS CUR for real amortized cost, so the spot discount shows up, not just list price
  • Idle % per node vs per cluster says when to consolidate harder (Lever 2) versus right-size (Lever 3)
What OpenCost actually charges a workload

Billing follows max(request, usage), so reserving capacity you never use is pure idle cost. Toggle the fix.

reserved node capacity · 1 vCPU1000m
used
idle
Requested
1000m
Used
250m
Idle
75%
OpenCost bills
1000m

Four cores reserved, one actually used. OpenCost bills max(request, usage) = the request, so the other 750m is idle cost booked against this workload, not the cluster. Waste with a name on it.

Same spend, sliced any way: the /allocation API aggregates bynamespacecontrollerpodlabelnode__idle__, so the biggest over-request always has a name attached.
OpenCost bills the greater of what a workload requests and what it uses, so the gap between the two is idle cost with a name on it. Toggle the VPA fix and watch the billed CPU fall.

Lever 5: Migrating legacy stacks onto Kubernetes

Here's the honest limit of everything above: it only helps a workload that runs on Kubernetes. Spot, consolidation, VPA, KEDA, OpenCost. None of it touches something sitting on ECS. So the last lever is unglamorous and very effective: find the legacy stacks and move them.

The clearest example was our OpenCTI threat-intelligence platform, which I'd originally stood up on AWS ECS. Migrating it to EKS brought it under the same spot-first, consolidated, right-sized regime as everything else and saved $130,200 a year on the AWS bill. One workload, one migration. It has run in production since 2022 with zero infrastructure incidents, so the saving didn't come at the cost of reliability.

The same spot-first economics, applied to accelerators
How ~80% spot works on a fleet of Inferentia, Trainium, and GPU nodes

Five levers, one bill

No single trick gets you from all-on-demand to a 60–70% lower bill. Spot sets the baseline, consolidation and scaling defend it, migration widens what it covers, and OpenCost tells you which lever to pull next. Pulled together, ~80% spot across the fleet turns into a durably cheaper platform that still holds its SLA, which is the only version of cost-cutting worth doing.

Five levers, one bill

No single trick gets you to 60–70%. It stacks. Walk the ledger.

Spot capacity· Interruption-tolerant tiers on spot, weighted NodePools, on-demand only where a reclaim would hurt.

The foundation. Running the fleet mostly on spot is what makes everything on-demand cost roughly 3× more per environment. Every other lever exists to protect or extend this one.

The compounding part: spot sets the baseline, consolidation and scaling defend it, migration widens what it covers, and OpenCost tells you which lever to pull next. Pull them together and ~80% spot turns into a 60–70% bill.
Walk the ledger: each lever compounds on the one before it, ending with the concrete $130,200/yr ECS→EKS migration.
Stack
KarpenterOpenCost / KubecostKEDAVPACluster Proportional AutoscalerNode Problem DetectorAmazon SQS