A Kubernetes cluster costs a knowable amount. Splitting that amount among the teams sharing it is not a measurement problem — the metering has been solved by OpenCost and its commercial descendants for years — it is a series of choices about fairness, each of which advantages some teams and disadvantages others. Whoever makes those choices is allocating blame as much as cost, and the teams on the receiving end will notice immediately whether the model was designed to be defensible or merely to sum correctly.

The failure mode is predictable. A platform team builds an allocation pipeline, sends the first showback report, and a senior engineer replies within an hour pointing out that their namespace is being charged for a third of the ingress controller despite serving four percent of the traffic. The number was arithmetically correct. The model was indefensible. Nobody reads the next report.

Decision one: requests or usage as the billing basis

This is the first fork and it determines the character of everything downstream.

Charging on usage — actual CPU seconds and memory bytes consumed — feels fair and is exactly wrong. Usage is not what the cluster pays for. The scheduler reserves capacity based on requests, and that reserved capacity is unavailable to anyone else whether it is used or not. A team that requests 40 cores and uses 4 has consumed 40 cores of schedulable capacity. Billing them for 4 makes their over-provisioning free, which means it will continue forever, and it pushes the cost of their waste onto teams who sized carefully.

Charging on requests aligns the incentive with the constraint. The bill goes down when the request goes down, and the request going down is the thing that actually frees capacity.

The refinement that handles the obvious objection — bursty workloads that legitimately exceed their requests — is to bill on max(request, usage) per container per time interval. A team cannot escape by under-requesting and burstng into node slack, and cannot be punished for a request that genuinely reflects its reservation. This is what OpenCost does by default and it is the right default.

# the allocation basis, per container, per hour
cpu_hours = max(
  avg_over_time(kube_pod_container_resource_requests{resource="cpu"}[1h]),
  avg_over_time(rate(container_cpu_usage_seconds_total[5m])[1h:5m])
)

Decision two: what happens to idle

Here is the arithmetic that starts arguments. A cluster costs $100,000 a month. Sum every workload's allocated capacity and you get $62,000. The missing $38,000 is idle — provisioned nodes that nobody's requests are sitting on, plus system reserved, plus DaemonSets, plus the control plane. Somebody has to be charged for it.

There are three models and they encode three different theories of responsibility.

ModelIdle goes toSignal it sendsFailure mode
ProportionalEvery team, pro rata on allocation"Idle is everyone's problem"Efficient teams subsidise the cluster's packing failures; nobody can act on it
Platform-absorbedThe platform team's own budget"Packing is the platform's job"Platform budget looks enormous; needs a mature exec conversation
AttributedThe team whose constraints caused it"Your GPU taint made this node unpackable"Hard to compute; disputes about causality

My position: platform-absorbed, with attributed exceptions. Idle capacity in a shared cluster is overwhelmingly a consequence of decisions the platform team owns — node shape selection, autoscaler configuration, headroom policy, bin-packing strategy. Charging application teams for it is charging them for something they cannot influence, and any cost signal a team cannot act on is noise that trains them to ignore the whole report.

The exception is idle that a specific team demonstrably caused. If a team requires a dedicated node pool — a taint for a licensing constraint, a GPU pool, a compliance isolation requirement — then the idle capacity in that pool is theirs. They asked for a boundary, the boundary has a cost, and showing them the cost is the only way the boundary ever gets re-examined. In practice this splits idle into two buckets and the attributed one is usually 10 to 25 percent of the total.

Absorbing idle also gives the platform team the correct incentive. Their budget line is now the difference between what the cluster costs and what teams are charged, which is precisely the number that better packing and autoscaling work reduces. That is a healthy thing to be measured on.

Decision three: shared services

Ingress controllers, service mesh control planes, cert-manager, external-dns, the observability stack, the CI runners. These are real costs with no natural owner.

The tempting move is to split them evenly across teams, which is fast, arithmetically clean, and wrong in the same way proportional idle is wrong. The defensible approach is a tiered one:

  • Metered where a natural unit exists. The observability stack bills on ingested bytes and active series per namespace — both directly measurable, both directly controllable by the team, and both the actual cost drivers. This is the single highest-value shared-cost attribution you can build, because telemetry spend responds fast to attribution.
  • Proportional to a proxy where metering is impractical. The ingress controller bills on request count per Ingress resource. The mesh control plane bills on proxy count per namespace. Neither is a perfect cost model, but each is a plausible causal driver and teams accept them.
  • Absorbed where no proxy is honest. cert-manager costs $40 a month. Do not build a model for it. The credibility cost of a contrived attribution exceeds the amount in dispute.

Write down which bucket each shared service is in and publish that list. Half the disputes evaporate when people can see the reasoning rather than inferring it from a number.

Making it computable

Allocation requires a label taxonomy, and the taxonomy has to be enforced at admission or it will be 70 percent complete forever. Three labels are enough:

metadata:
  labels:
    cost.scaleops.ai/team: payments          # who pays
    cost.scaleops.ai/service: checkout-api   # what it is
    cost.scaleops.ai/env: prod               # which envelope

Enforce with a validating policy so unlabelled workloads cannot reach production:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-cost-labels
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-team-label
    match:
      any:
      - resources:
          kinds: ["Deployment","StatefulSet","CronJob","Job"]
          namespaceSelector:
            matchLabels: { env: prod }
    validate:
      message: "cost.scaleops.ai/team label is required"
      pattern:
        spec:
          template:
            metadata:
              labels:
                cost.scaleops.ai/team: "?*"

Run this in Audit for a month first, publish the list of violations to the owning teams, then flip to Enforce. Flipping straight to Enforce on a live cluster is how platform teams acquire reputations.

For the underlying node cost, do not use list price. Pull the amortised rate from the Cost and Usage Report so that Savings Plan and Reserved Instance coverage flows through to the per-team number. Otherwise your allocation totals will not reconcile to the invoice, and the first time a finance partner notices that gap, the entire report loses standing.

Showback, chargeback, and the thing in between

Showback is reporting with no financial consequence. Chargeback moves real money between cost centres. Most organisations should run showback and stop there, because chargeback's overhead — dispute resolution, budget transfers, monthly true-ups, an internal transfer pricing policy — is substantial and the behavioural gain over well-run showback is small.

The version that outperforms both is budget-with-consequence: each team gets a quarterly infrastructure envelope, no money changes hands, but exceeding it requires a written justification to the same forum that approves headcount. It produces the behaviour change of chargeback with a fraction of the machinery, and it puts the conversation where it belongs — alongside other resource allocation decisions rather than in a finance reconciliation.

Whatever you send, three properties determine whether it gets read. It must reconcile to the invoice, to within a percent. It must show change and its cause, not just level — "up $8,400, of which $7,900 is the new embeddings pipeline in staging" is actionable in a way that a pie chart is not. And it must arrive somewhere engineers already are: a Slack channel, a PR comment on the manifest that changed the request, a panel in the team's existing dashboard. A monthly PDF is a compliance artifact, not a feedback loop, and the distinction between those two is most of what separates a functioning FinOps practice from a reporting one.

Get the model wrong and no amount of dashboard polish rescues it. Get it right and it will survive being argued with, which is the only test that matters.