Kubernetes
Kubernetes cost allocation that reconciles to the bill
Allocations built from list prices drift away from the invoice, and finance stops trusting them. How to split node cost from billed, net amortized cost, charge pods fairly, and show what belongs to nobody.
UnitVane Engineering
October 6, 20267 min read
A common first attempt at Kubernetes cost allocation multiplies each pod's CPU and memory requests by a public on-demand rate. The numbers look precise, down to the cent per pod. Then finance adds them up and they disagree with the invoice, because the invoice includes Savings Plans, reservations, negotiated discounts and credits, and the price sheet does not.
Once a total disagrees with the bill, every number underneath it is disputed, and the allocation program stalls. It matters: after waste reduction, allocation of cloud spend is the next priority for FinOps practitioners in the FinOps Foundation's 2025 survey (FinOps Foundation, State of FinOps 2025). The first rule of allocation is simple: start from what you were billed.
Why list-price allocation drifts
| What the price sheet leaves out | Effect on a list-price allocation |
|---|---|
| Savings Plans and reservations | Covered hours look full price; the commitment fee appears elsewhere on the bill |
| Negotiated and enterprise discounts | Every pod looks more expensive than it was |
| Credits and refunds | Allocated total exceeds what was paid |
| Spot and preemptible pricing | Nodes billed at a fraction of on-demand look full price |
| Volumes, load balancers, data transfer | Missing entirely unless priced separately |
| Capacity no pod requested | Disappears, or is silently spread across teams |
Start from the billed cost of each node
Every major provider exports billing at resource level: AWS Cost and Usage Report (CUR 2.0), Azure Cost Management exports, and the Google Cloud detailed billing export in BigQuery. The FinOps Foundation's open FinOps Foundation, FOCUS specification format describes the same data in one schema. Join each node to its line items by provider ID and use the net amortized cost: commitments spread over the hours they cover, discounts and credits applied.
That number is what the node really cost you that day. Everything that follows only divides it up, so the parts always add back to the bill.
Split on max(request, usage)
Charging only for usage lets teams reserve large requests for free: the scheduler still holds that capacity for them. Charging only for requests makes a pod that bursts above its request look cheaper than it is. Charging the higher of the two, per resource and per hour, matches what each pod took from the node.
AWS's own split cost allocation data for EKS works from the instance's amortized cost and the share of CPU and memory each pod consumed (AWS, Understanding split cost allocation data). With usage metrics enabled, it uses the greater of each pod's requested and actual usage (AWS Cloud Financial Management, Improve cost visibility of Amazon EKS with AWS split cost allocation data), so the two approaches can be cross-checked.
Here is one node for one day. It is billed $12.00 net, with 8 vCPU and 32 GiB of memory. Split 50/50 between CPU and memory, that is $0.75 per vCPU-day and $0.1875 per GiB-day.
| Pod | CPU request / used | Memory request / used | Charged (the higher of each) | Cost for the day |
|---|---|---|---|---|
| checkout | 2 / 1 vCPU | 4 / 3 GiB | 2 vCPU, 4 GiB | $2.25 |
| search | 1 / 2.5 vCPU | 8 / 6 GiB | 2.5 vCPU, 8 GiB | $3.375 |
| batch | 1 / 0.5 vCPU | 4 / 2 GiB | 1 vCPU, 4 GiB | $1.50 |
| Infrastructure idle | 2.5 vCPU unclaimed | 16 GiB unclaimed | $4.875 | |
| Billed | $12.00 |
Note what the rule does to each team. search uses more CPU than it requested and pays for what it used. checkout and batch requested more than they used and pay for their requests. And 40% of the node's cost is visible as idle instead of being spread invisibly across all three.
- Split node cost between CPU and memory first; 50/50 is a reasonable default you can tune.
- Charge each pod its share of max(request, usage) for each resource, for each hour it ran.
- Keep GPU, persistent volume, network and load balancer cost in their own columns.
Keep idle and shared cost visible
Whatever no pod claimed is idle. Platform namespaces such as kube-system, monitoring and ingress-nginx are shared. Both are real costs, and hiding them inside team numbers is how allocation loses trust. Report them in their own columns, then decide who pays with an explicit policy.
| Policy | How it works | Good fit when |
|---|---|---|
| Platform absorbs | Idle and shared stay with the platform team | The platform team owns node sizing and wants the incentive |
| Proportional | Redistributed in proportion to each team's direct cost | You want team totals to add up to the whole bill |
| Hybrid | A share stays with the platform, the rest is proportional | Some idle is a deliberate safety margin, some is waste |
Date your policies
Show what you could not attribute
Some billed resources will not join to anything the cluster reports: a volume whose PersistentVolume was deleted, a node that ran between snapshots, a load balancer with no matching Service. Do not spread that cost into idle. List it as unallocated, with the resource IDs, so it becomes a list of fixes rather than a gap in the total.
Reconcile every day, with two numbers
A single percentage cannot say both "the total is right" and "everything has an owner". Report them separately, for the same clusters, period and cost basis:
Reconciled to invoice = (direct + shared + idle + unallocated) / billed cost in scopeOwnership coverage = cost that resolves to a team or cost center / billed cost in scopeA cluster can be 100% reconciled and 89% owned at the same time: every dollar is accounted for, and the unowned part is a named list of resources waiting for a label.
Who does what
How a cloud charge becomes a team cost
- 01
Read the bill
Import net amortized charges or use your data center rate card.
Billing source
- 02
Match resources
Join billed nodes, volumes and load balancers with cluster inventory.
KubeOn hub
- 03
Allocate + assign
Split node cost using max(request, usage), then apply ownership and shared-cost policies.
Allocation engine
- 04
Reconcile
Review billed versus allocated totals and show unallocated spend separately.
FinOps review
Read-only by default. KubeOn changes a cluster only where you grant write access, feature by feature.
Read the workflowA checklist
- Billing export with resource IDs, cataloged and queryable.
- Node cost from net amortized line items.
- Pod cost on max(request, usage), per resource, per hour.
- Idle and shared in their own columns, with an effective-dated policy.
- Unallocated listed with resource IDs.
- A daily reconciliation per cluster, and ownership coverage beside it.
Why not allocate on usage only?
Because the scheduler reserves requests. A team that requests 4 vCPU and uses 1 still blocks 4 vCPU from everyone else, and the node still costs the same.
What if billing data arrives late?
Providers restate recent line items. Re-read the last few days of billing every day, and re-allocate a day once both its usage and its billing are in. Old days should be re-run deliberately, and logged.
Does this work for data center clusters?
Yes, with a rate card or amortized hardware cost in place of the invoice. See pricing Kubernetes in your own data center.
How does KubeOn do it?
Exactly as described: net amortized node cost, a 50/50 CPU and memory split by default, max(request, usage) per hour, idle and shared in their own columns, unallocated with up to 200 resource IDs, and both daily figures. Where AWS split cost allocation data exists, KubeOn uses AWS's per-pod cost directly. See the cost allocation methodology.