Skip to content

The cost model ​

AWS bills you for a whole instance. Kubernetes schedules fractions of that instance to pods. Bridging the two is the entire job of the cost model, and it comes down to three questions: what does the node cost, how do you divide that between CPU and memory, and how long was the node actually running.

1. What the node costs ​

Each node group — a distinct (region, AZ, instance type, capacity type) — is priced once per query:

  • On-demand: AWS Price List API, filtered by service, location, instance type, OS, tenancy, preInstalledSw=NA, capacitystatus=Used.
  • Spot: EC2 spot price history over the last 6 hours, newest timestamped sample for that AZ and OS.

Spot never silently falls back to on-demand. If spot history returns nothing, the price is unavailable and the node group lands in coverage.unpriced.

2. Splitting the price across CPU and memory ​

An instance price is a single number covering both vCPU and memory. To charge a pod for the CPU and memory it requested, that number has to be decomposed. The weighting is 9:1, vCPU to GiB:

weighted        = (1 × memoryGiB) + (9 × vCPU)
unit            = pricePerHour / weighted
costPerVCPUHour = 9 × unit
costPerGiBHour  = 1 × unit

The 9:1 ratio is taken from AWS Fargate's published pricing, which is AWS's own statement of the relative value of the two resources, and matches the approach in AWS split cost allocation data for EKS. It is not a KubeSpend invention.

Worked example, m6a.large (2 vCPU, 8 GiB) at $0.0556/hr:

weighted        = 8 + (9 × 2) = 26
unit            = 0.0556 / 26   = 0.0021385
costPerVCPUHour = 9 × 0.0021385 = $0.019246
costPerGiBHour  = 1 × 0.0021385 = $0.002138

Sanity check: 2 × 0.019246 + 8 × 0.002138 = 0.0556. The split always sums back to the instance price.

3. Node-hours, counted not assumed ​

nodeCost = nodeHours × pricePerHour

nodeHours is the count of distinct (node, hour) observations — not nodeCount × 24. This matters for autoscaled and spot fleets: a node that lived for one hour is charged for one hour. An earlier model assumed every node ran a full day and over-billed short-lived nodes 24-fold.

The consequence is that a partially-observed day reports less cost than reality. That is the honest direction to be wrong in, and coverage.partial flags it when it happens.

4. Splitting the daily total by resource ​

The daily CPU/memory split uses the same shares the price was decomposed with, so the two always reconcile:

cpuShare = (vCPU × costPerVCPUHour) / (vCPU × costPerVCPUHour + memGiB × costPerGiBHour)
cpuUSD   = nodeCost × cpuShare
memUSD   = nodeCost × (1 − cpuShare)

If the instance shape is unknown the split falls back to the raw weight ratio (9/10 and 1/10) so it stays proportional rather than arbitrary.

5. Storage, billed on top ​

An EC2 instance price covers vCPU and memory only. The root volume is a separate EBS charge, and leaving it out understates every node.

gib            = ceil(nodeStorageCapacityBytes / 2^30)
perGiBMonth    = EBS price for the region (gp3 by default)
rootVolumeCost = nodeHours × gib × (perGiBMonth / 730)

730 is the mean hours in a month, matching how AWS quotes monthly prices. Charging per node-hour means a node that ran half a day is charged for half a day of disk.

PersistentVolume cost is added on top from the agent's PV inventory, at monthlyPrice / 30.44 per day.

Two caveats: the root volume type is assumed gp3 (the Kubernetes node object does not expose it, and gp3 is the default for EKS-optimised AMIs and Karpenter), and the size is inferred from ephemeral-storage capacity, which is slightly smaller than the provisioned volume.

6. Discounts ​

Every cost figure in the console is at list price today. There is not yet an organization setting for an Enterprise Discount Program (EDP) percentage, so dashboard, attribution and recommendation figures do not reflect one. The pricing API accepts a per-request EDP percentage (validated as 0 < d ≤ 100 and ignored rather than clamped if malformed), which is what an org-level setting will build on.

Reserved Instances and Savings Plans are not modelled.

7. Network transfer, added at the cluster level ​

With the eBPF agent installed, two flow buckets are priced per day and added to the cluster's daily cost:

  • cross-AZ (intra-region) transfer at a flat per-GB rate, already counted per direction by the flow query
  • internet egress on tiered rates, egress direction only

Same-AZ traffic is free, so it contributes nothing. Traffic where one end has no resolvable zone is not priced at all — calling it same-AZ or cross-AZ would be inventing an answer — so coverage carries networkPricedBytes, networkFreeBytes and networkUnclassifiedBytes, and a large unclassified share means the network figure is a floor rather than a total.

networkUsd stays null, not 0, when no flow records exist or when no region could be resolved for the cluster's nodes. A network query failure is logged and dropped rather than failing the whole breakdown, so compute cost still returns.

What this model does not include ​

  • NAT gateway and inter-region transfer. Rates exist in the table; nothing produces those buckets, so that spend is absent.
  • Per-workload network dollars. Network cost is cluster-level and daily; the per-workload network output is bytes split by zone.
  • Reserved Instances and Savings Plans.

See What we measure.

Next ​

Every figure in KubeSpend traces to a real cloud price. Where we cannot measure something, we say so.