Cloud Infrastructure · Cost Engineering
Allocating Shared Cost to a Unit of Work
Why dividing a cluster bill by resource requests overcharges some tenants, how idle capacity hides in an allocation model, and where the normalization factor comes from.
A bill arrives with one line for a cluster of machines. Something inside that cluster serves four services, two batch jobs, and a search index, and the question is what each of them cost. There is no measurement that answers this. The provider meters the cluster as a whole, and every finer-grained figure is produced by dividing a cost pool with an allocation key. The choice of key is the entire model, and different keys disagree by large factors on the same workload.
The cost pool and the allocation key are separate decisions
A cost allocation has two inputs. The pool is the money to be divided: the cluster’s instance charges, its storage, its network, plus anything that must be loaded onto it such as support fees or the amortized price of a commitment. The key is the quantity that determines each consumer’s share. Pool times share equals allocated cost.
The pool is usually easy and the key is usually wrong. Three keys are common, and they answer different questions.
Allocating by resource request divides the pool in proportion to what each consumer reserved. Allocating by measured usage divides it in proportion to what each consumer actually consumed. Allocating by a business driver, such as requests served or records processed, divides it in proportion to output. The first two describe the machine; the third describes the product. A team that is charged by request volume will optimize request count, and a team charged by CPU-seconds will optimize CPU, and those are not the same objective.
Resource requests have one property that makes them attractive for chargeback: they are declared in configuration before the fact, so a team can compute its own bill without reading anyone else’s telemetry. That property is also the source of the largest error in the model.
Requests are not capacity, and the difference has to go somewhere
In a system that schedules by request, the sum of requests on a node may legitimately exceed the node’s allocatable capacity, because requests are a scheduling guarantee rather than a reservation of physical resource. Overcommit ratios of two or three to one are ordinary for memory and higher for CPU. If allocation divides the pool in proportion to raw requests, the allocated amounts sum to more than the pool, and the excess is not a rounding error: it can exceed the bill.
The fix is a normalization factor computed from the actual population, not assumed:
def allocate(pool, requests):
"""requests: {tenant: requested_units}; returns cost per tenant, summing to the pool."""
reserved = sum(requests.values())
# Scale so allocations sum to the pool: the key is share of *reserved*, not of capacity.
scale = pool / reserved
return {t: r * scale for t, r in requests.items()}
def allocate_against_capacity(pool, requests, capacity_total):
"""Alternative: price reserved units at the capacity rate, and surface the residual."""
rate = pool / capacity_total
alloc = {t: r * rate for t, r in requests.items()}
return alloc, pool - sum(alloc.values())
The two functions answer different questions. The first always sums to the pool and therefore silently absorbs both overcommit and idle capacity into a per-unit rate. The second prices a reserved unit at the true cost of a capacity unit, and returns a residual equal to the cost of capacity nobody reserved. On a node with 16 vCPU where requests total 10, the residual is 37.5 percent of the pool. That residual is the cost of headroom, and the decision of who pays it is a policy choice with real consequences.
Hiding it inside the rate is the common default, and it has a specific distortion. Tenants with identical usage patterns pay the same rate regardless of how much slack the cluster carries, so the cluster’s inefficiency is invisible to everyone who could fix it. Surfacing it as a separate line, charged to the platform owner or distributed by a business key, makes slack a visible budget item. Neither is correct in the abstract; the failure mode is choosing one and forgetting that a per-unit rate is a ratio of two changing quantities.
Measured usage has its own denominator problem
Allocating by measured usage seems more honest, since it charges for what was consumed. It fails in a different place: usage samples are instantaneous, and the sum of samples across tenants is not the capacity they collectively held. A tenant that runs a batch job for ten minutes at full CPU and is idle for the other fifty consumes the same CPU-seconds as one that runs at a sixth of a core for an hour, but the first held a full core for ten minutes and the second held a sixth for sixty. If the cluster was provisioned for the first, the second’s usage-based share understates the capacity it caused to exist.
The general form of this problem is that cost is driven by the maximum concurrent requirement over a provisioning interval, while usage is an average over the same interval. Any allocation by average under-charges the bursty consumer and over-charges the steady one, and the size of the gap is the ratio between peak and mean. A workload with a 10:1 peak-to-mean ratio is charged one tenth of the capacity its peak forced.
A practical construction charges each tenant twice: a reservation component for the capacity it holds, priced at the capacity rate, and a consumption component for usage beyond its reservation, priced at a marginal rate. The two components are not arbitrary. The reservation component recovers the fixed cost that exists because someone had to reserve; the consumption component recovers the variable cost of shared pools.
Granularity determines what can be attributed at all
Attribution cannot be finer than the metering. If the provider bills a cluster as a whole and internal telemetry samples every fifteen seconds, then a workload that exists for three seconds is invisible in the internal data while still contributing to the bill. The unattributed remainder grows with the churn rate, and a platform that creates and destroys many short-lived containers accumulates a gap that no allocation key can close.
The standard treatment is an explicit unallocated bucket, reported rather than distributed. Its size is a measurement of attribution quality: a bucket that is a small percentage of the pool means the key covers the population, and a bucket that is large means the model is describing a fraction of reality. Distributing the bucket across tenants to make the numbers sum to the invoice converts a visible measurement into an invisible error.
A second granularity limit comes from shared infrastructure. Load balancers, control planes, logging collectors, and the monitoring stack are not attributable to any single workload by construction. Their cost is real and belongs to the platform, and charging it out by a usage key asserts a causal relation that does not exist. Loading it as a percentage of direct cost is a defensible convention as long as the convention is stated; loading it as a percentage and calling the result the workload’s cost is not.
The fully loaded rate is a decision input, not a fact
The output of allocation is a per-unit rate, and that rate is routinely used for a decision it cannot support. A team comparing two architectures wants the cost per request, computes it as total allocated cost divided by requests, and concludes that the cheaper allocation is the cheaper architecture. The rate used in that comparison includes fixed capacity, shared platform cost, and slack. If the alternative architecture would run on the same cluster, none of those costs change, and the true difference is the marginal cost alone.
The distinction has a concrete form. Suppose a cluster costs a fixed amount per month and serves a load that varies by a factor of three between peak and trough. The average cost per request at trough is three times the average cost per request at peak, purely because the denominator moved. A comparison made in the trough month will reject a change that is beneficial, and a comparison made in the peak month will accept one that is not. The average rate is a description of a past interval, and the marginal rate is what a change actually alters. The relationship between the two is worked through in commitment instruments and break-even utilization, where the marginal price is set by contract rather than by load.
A workable rule is to report both: the fully loaded rate for budgeting, and the marginal rate for decisions. The fully loaded rate answers how much money the organization must find. The marginal rate answers whether a change reduces that amount. Reporting only the first produces an organization that optimizes its own accounting rather than its spending, and reporting only the second produces a budget nobody can reconcile.
What the allocation model cannot claim
Every allocation figure is a division. It has no independent measurement behind it, so its accuracy is bounded by the accuracy of the pool and the relevance of the key, and its precision is an artifact of arithmetic. Two teams can each build a defensible model over the same invoice and produce per-service costs that differ by a factor of two, and neither model is falsifiable against a ground truth that does not exist.
What is falsifiable is the pool. Reconciling the sum of allocations against the invoice, including the unallocated bucket and the residual from capacity pricing, is a check that the model is internally consistent. Reconciling the invoice against exported usage records is a check that the extraction is complete. Both are worth doing before any per-unit figure is used to justify a decision, because a per-unit figure derived from an incomplete pool is wrong by exactly the missing fraction, and it is wrong in the direction that makes every service look cheaper than it is. The billable boundaries that most often go missing from a pool are the ones described in egress boundaries and billable data transfer.
References
- Kubernetes Documentation — Resource Management for Pods and Containers — Kubernetes project
- FinOps Framework — FinOps Foundation
- The Datacenter as a Computer: An Introduction to the Design of Warehouse-Scale Machines — Barroso and Hoelzle, Morgan and Claypool