---
title: 'Cost Visibility'
source: 'https://academia.sh/en/courses/platform-engineering/cost-visibility'
course: 'Platform Engineering'
language: en
updated: '2026-09-13T05:08:29+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Splits total cost across five teams and separately counts directly assignable, shared, and unallocatable items'
    - 'Sweeps three allocation rules: measures which team each rule misleads and in which direction, as a deviation'
    - "Measures visibility's effect on behavior and separates by name the item that is not under a team's control"
---

# Cost Visibility

Team-based resource accounting: only 47.5% of a daily 1,376 fictional units can be directly assigned, each of three allocation rules misleads a different team in a different direction (the equal-share rule loads field +79.0 while the request rule loads meter-reading +140.7), only 15.6% of the bill drops after cost is shown while 29.7% never moves at all, and central accounting pays for itself at three teams but two teams opting out drags the five-team gain of 85 units down to 3 units.

The previous lesson embedded the rules in the pipeline: five teams complied without ever opening the
policy engine, the number of exemptions was counted, and the rule's invisibility was written down as a
gain. For those same five teams, one thing is still invisible — **how much they spend**. The platform
buys the resource and reads the bill on their behalf.

Unit cost, idle resources, and economies of scale were measured in M22/K06; here that bill is the
input. This lesson's question is a single division: how is the same amount split across **five teams**,
how much cannot be split, and what changes once the split amount is shown to each team.

## Splitting the Bill Across Five Teams

The example is again the fictional **regional measurement network**, and the five teams are fictional:
meter reading, validation, billing, field, reporting. The line items, prices, and usage measures are
fictional; **the currency is fictional too** and is called `u`, and it does not correspond to any real
price list.

**PC25:** there are nine cost items, and each item's daily cost is written down. **PC26:** items
are three types — **directly** assignable (carries a team tag), **shared** (cluster control plane,
shared build and deploy pipeline, monitoring stack), and **unallocatable**; the last of these has no
relationship to any team's usage and is paid even with no team. **PC27:** a shared item's **actual
driver** is known in the model (concurrent jobs, build minutes, series count) but is not in the bill.
**PC28:** allocation rules can use only measures readable from the bill: equal share, observed
resource-usage ratio, request count. **PC29:** once cost is shown to a team, each item drops by its own
**audit share**; this is a single-period reaction. **PC30:** the central accounting pipeline has a fixed
daily cost, and so does a team standing up its own accounting.

Teams, items, and the bill are all a **model** built within the lesson; there is no real bill.

```js
// cost/item.mjs — team-based accounting model: there is no real bill. Teams, line items, and
// prices are a data structure; the currency is fictional and is called `u`, and it does not
// correspond to any real price list. A shared item's *actual driver* is known in the model,
// it is not in the bill; allocation rules can only use measures readable from the bill.

export const TEAM = ["meter-reading", "validation", "billing", "field", "reporting"];
export const N = TEAM.length;

// directly assignable: name, daily u per team, drop share after visibility
export const DIRECT = [
  ["service instances",         [96, 54, 120, 38, 72], 0.18],
  ["block storage",             [24, 18,  40, 12, 58], 0.10],
  ["temporary environment copies", [30, 22, 26, 10, 34], 0.55],
];

// shared: name, daily u, driver name, driver values (known in the model), drop share
export const SHARED = [
  ["cluster control plane", 210, "concurrent jobs", [34, 12, 28, 6, 9],           0.00],
  ["shared build and deploy", 168, "build minutes",  [220, 180, 260, 40, 610],     0.12],
  ["monitoring stack",      146, "series count",    [8200, 3100, 5400, 900, 2600], 0.30],
];

// unallocatable: has no relationship to any team's usage; paid even with no team
export const UNALLOCATABLE = [
  ["regional backbone network", 88], ["audit log retention", 46], ["standby region", 64],
];

// team measures readable from the bill
export const METRIC = {
  usage: [0.31, 0.17, 0.29, 0.08, 0.15],   // observed resource-usage ratio
  requests: [412, 96, 138, 21, 33],        // daily thousand requests
};

export const CENTRAL = 120;   // fixed daily cost of the central accounting pipeline (u)
export const PER_TEAM = 41;   // daily cost of a team standing up its own accounting (u)

export const sum = (v) => v.reduce((a, b) => a + b, 0);
export const ratio = (v) => { const t = sum(v); return v.map((x) => x / t); };

export const RULE = {
  equal: () => TEAM.map(() => 1 / N),
  usage: () => ratio(METRIC.usage),
  requests: () => ratio(METRIC.requests),
};

// each shared item's *actual* per-team share: every item is split by its own driver
export const actualShare = () => TEAM.map((_, e) =>
  sum(SHARED.map(([, u, , d]) => u * ratio(d)[e])));

// the deviation a rule produces on shared items: charged minus actual
export const deviation = (rule) => {
  const p = RULE[rule](), totalU = sum(SHARED.map((x) => x[1])), actual = actualShare();
  return TEAM.map((_, e) => totalU * p[e] - actual[e]);
};
```

```js
// cost/measure.mjs — item breakdown, the deviation of three allocation rules, visibility's
// effect on behavior, and central accounting's threshold. All numbers derive from the model above.
import { TEAM, N, DIRECT, SHARED, UNALLOCATABLE, RULE, CENTRAL, PER_TEAM,
  sum, deviation } from "./item.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const t0 = process.hrtime.bigint();

const D = sum(DIRECT.map((x) => sum(x[1])));
const P = sum(SHARED.map((x) => x[1]));
const B = sum(UNALLOCATABLE.map((x) => x[1]));
const T = D + P + B, pctOf = (a) => `${(100 * a / T).toFixed(1)}%`;

const COUNT = DIRECT.length + SHARED.length + UNALLOCATABLE.length;
console.log(`${N} teams, ${COUNT} items, daily ${T} u (fictional currency)`);

console.log("\n1. item breakdown: what is directly assigned, what is shared, what is unallocatable");
const A = [-30, 17, 11, 9, 18, 9];
write(A, "item", "type", "daily u", "share", "actual driver", "audit");
for (const [name, v, d] of DIRECT)
  write(A, name, "direct", sum(v), pctOf(sum(v)), "team tag", `${(100 * d).toFixed(0)}%`);
for (const [name, u, driver, , d] of SHARED)
  write(A, name, "shared", u, pctOf(u), driver, `${(100 * d).toFixed(0)}%`);
for (const [name, u] of UNALLOCATABLE)
  write(A, name, "unallocatable", u, pctOf(u), "none", "0%");
write(A, "total", "-", T, "100%", "-", "-");
console.log(`  direct ${D} u (${pctOf(D)}), shared ${P} u (${pctOf(P)}),`);
console.log(`  unallocatable ${B} u (${pctOf(B)}); not directly assignable ${pctOf(P + B)}`);

console.log("\n2. allocation rule sweep: charged minus actual (u/day, shared items)");
const C = [-11, ...TEAM.map(() => 15), 12];
write(C, "rule", ...TEAM, "abs. total");
const score = {};
for (const k of Object.keys(RULE)) {
  const s = deviation(k);
  score[k] = sum(s.map(Math.abs));
  write(C, k, ...s.map((x) => (x >= 0 ? "+" : "") + x.toFixed(1)), score[k].toFixed(1));
}
const best = Object.keys(score).reduce((a, b) => (score[a] <= score[b] ? a : b));
for (const k of Object.keys(RULE)) {
  const s = deviation(k), i = s.indexOf(Math.max(...s)), j = s.indexOf(Math.min(...s));
  console.log(`  ${k.padEnd(9)} charges most to ${TEAM[i]} +${s[i].toFixed(1)}, ` +
    `saves most for ${TEAM[j]} ${s[j].toFixed(1)}`);
}
console.log(`  least wrong rule: ${best}`);
console.log(`  every rule also spreads the ${B} u unallocatable item; that amount's deviation`);
console.log("  cannot be measured, because there is no actual share to compare against");

console.log("\n3. visibility's effect on behavior: after cost is shown to a team");
const E = [-30, 11, 9, 11, 30];
write(E, "item", "daily u", "audit", "dropped u", "reason it stays fixed");
const items = [
  ...DIRECT.map(([name, v, d]) => [name, sum(v), d, "-"]),
  ...SHARED.map(([name, u, , , d]) => [name, u, d, d ? "-" : "platform decision"]),
  ...UNALLOCATABLE.map(([name, u]) => [name, u, 0, "not under the team's control"]),
];
let dropped = 0, fixedU = 0, fixedCount = 0;
for (const [name, u, d, reason] of items) {
  dropped += u * d;
  if (d === 0) { fixedU += u; fixedCount++; }
  write(E, name, u, `${(100 * d).toFixed(0)}%`, (u * d).toFixed(1), reason);
}
console.log(`  total drop ${dropped.toFixed(1)} u/day (${(100 * dropped / T).toFixed(1)}%)`);
console.log(`  never moves ${fixedCount} items, ${fixedU} u/day (${pctOf(fixedU)})`);

console.log("\n4. central accounting's threshold and the cost of opting out");
const F = [-8, 13, 15, 11, 18];
write(F, "teams", "central u", "separate u", "gain", "pays for itself");
for (const n of [1, 2, 3, 5, 8]) {
  const separate = n * PER_TEAM;
  write(F, n, CENTRAL, separate, (separate - CENTRAL).toFixed(0), separate > CENTRAL ? "yes" : "no");
}
const s = deviation(best), sorted = TEAM.map((e, i) => [e, Math.abs(s[i])])
  .sort((a, b) => b[1] - a[1]).slice(0, 2);
const optOut = sorted.length * PER_TEAM, gross = N * PER_TEAM - CENTRAL;
const threshold = Math.ceil(CENTRAL / PER_TEAM);
console.log(`  threshold ${threshold} teams; gross gain at ${N} teams ${gross} u/day`);
console.log(`  under the ${best} rule, the ones deviating most, ${sorted.map((x) => x[0]).join(" and ")}, stand up`);
console.log(`  their own measurement: ${optOut} u/day of duplicated measurement, net gain ` +
  `${gross - optOut} u/day`);

const ms = Number(process.hrtime.bigint() - t0) / 1e6;   // raw measurement is not printed, the band is audited
console.log(`\nfour tables and rule sweep in the 2000 ms band: ${ms < 2000 ? "yes" : "no"}`);
```

```
5 teams, 9 items, daily 1376 u (fictional currency)

1. item breakdown: what is directly assigned, what is shared, what is unallocatable
item                                       type    daily u    share     actual driver    audit
service instances                        direct        380    27.6%          team tag      18%
block storage                            direct        152    11.0%          team tag      10%
temporary environment copies             direct        122     8.9%          team tag      55%
cluster control plane                    shared        210    15.3%   concurrent jobs       0%
shared build and deploy                  shared        168    12.2%     build minutes      12%
monitoring stack                         shared        146    10.6%      series count      30%
regional backbone network         unallocatable         88     6.4%              none       0%
audit log retention               unallocatable         46     3.3%              none       0%
standby region                    unallocatable         64     4.7%              none       0%
total                                         -       1376     100%                 -        -
  direct 654 u (47.5%), shared 524 u (38.1%),
  unallocatable 198 u (14.4%); not directly assignable 52.5%

2. allocation rule sweep: charged minus actual (u/day, shared items)
rule         meter-reading     validation        billing          field      reporting  abs. total
equal                -62.9          +31.0          -33.6          +79.0          -13.5       220.0
usage                 -5.3          +15.3          +13.5          +16.1          -39.7        89.8
requests            +140.7           -1.9          -35.1          -10.1          -93.6       281.4
  equal     charges most to field +79.0, saves most for meter-reading -62.9
  usage     charges most to field +16.1, saves most for reporting -39.7
  requests  charges most to meter-reading +140.7, saves most for reporting -93.6
  least wrong rule: usage
  every rule also spreads the 198 u unallocatable item; that amount's deviation
  cannot be measured, because there is no actual share to compare against

3. visibility's effect on behavior: after cost is shown to a team
item                              daily u    audit  dropped u         reason it stays fixed
service instances                     380      18%       68.4                             -
block storage                         152      10%       15.2                             -
temporary environment copies          122      55%       67.1                             -
cluster control plane                 210       0%        0.0             platform decision
shared build and deploy               168      12%       20.2                             -
monitoring stack                      146      30%       43.8                             -
regional backbone network              88       0%        0.0  not under the team's control
audit log retention                    46       0%        0.0  not under the team's control
standby region                         64       0%        0.0  not under the team's control
  total drop 214.7 u/day (15.6%)
  never moves 4 items, 408 u/day (29.7%)

4. central accounting's threshold and the cost of opting out
teams       central u     separate u       gain   pays for itself
1                 120             41        -79                no
2                 120             82        -38                no
3                 120            123          3               yes
5                 120            205         85               yes
8                 120            328        208               yes
  threshold 3 teams; gross gain at 5 teams 85 u/day
  under the usage rule, the ones deviating most, reporting and field, stand up
  their own measurement: 82 u/day of duplicated measurement, net gain 3 u/day

four tables and rule sweep in the 2000 ms band: yes
```

The numbers are of the **measurement** class; their inputs are the assumptions above.

## Half the Bill Cannot Be Directly Assigned

The first table splits the daily 1,376 units into three types. Directly assignable items add up to 654
units (47.5%): service instances, block storage, and temporary environment copies carry a team tag, and
which team used how much is readable from the bill. The remaining **52.5%** is not.

Of that, 524 units (38.1%) are **shared** items. The cluster control plane, the shared build and deploy
pipeline, and the monitoring stack are each run once for all five teams; this is exactly the layer the
platform took over. It appears as a single lump sum, and belongs to nobody.

That leaves 198 units (14.4%) of **unallocatable** items. The regional backbone network, audit log
retention, and the standby region cost the same amount whether there are three teams or five; no team's
departure shrinks them. This item has no owner and no driver — when an allocation rule is applied to it,
the number it produces is not a **measurement**, it is a **decision**.

## Each Rule Misleads a Different Team in a Different Direction

The second table sweeps the three rules over the shared 524 units. The point of comparison is the
**actual share** known in the model: each shared item is split by its own driver. The deviation is the
amount the rule charges minus the actual share; a plus sign shows the team was overcharged, a minus sign
shows it got off easy.

**Equal share** gives the crudest result (absolute total 220.0). The field team is overcharged **+79.0
units**: it is the smallest of the five teams, its concurrent jobs are six, its build minutes are forty,
its series count is nine hundred — yet it pays one-fifth. Meter reading gets off **−62.9 units**; it is
the heaviest user of the cluster control plane but never sees that in its bill.

**Usage ratio** is the least wrong rule (89.8), but it is not one-directional: reporting gets off by
**−39.7**, because even though its resource usage is low, it alone accounts for 46% of build minutes.
The observed usage ratio never sees the build pipeline's driver at all.

**Request count** is the most misleading rule (281.4). Meter reading is charged **+140.7 units** because
it has the highest count, 412 thousand requests a day; yet its cost per request is low. Reporting gets
off by **−93.6**: its nightly batch job never turns into a request.

The three rules mislead three different teams in three different directions, and none corrects another's
error. On top of that, the 198-unit unallocatable item is still spread out under every rule, and that
amount's deviation **cannot be measured** — there is no actual share to compare it against. Under
**showback** this is a tolerable wrong: the number is shown, nobody pays it. Under **chargeback** the
same number comes out of a budget, and the argument shifts onto the rule itself.

## What Visibility Moves

The third table shows the cost to each team and counts what drops. The total drop is **214.7 units**
(15.6%). The drop does not come from where it is expected: the highest response rate is in **temporary
environment copies** (55%, 67.1 units) — a team can delete an unused environment itself. Service
instances drop a similar amount thanks to their size (68.4 units), but their rate is only 18%; changing
size requires a deployment. The monitoring stack drops 30%, because a team can thin out its own tags.

Four items **never move at all**: 408 units, **29.7%** of the bill. The cluster control plane is a
platform decision; the regional backbone network, audit log retention, and standby region are not under
any team's control — audit log retention, in fact, is itself the cost of the policy rule from the
previous lesson. Showing these items on a team's bill produces no drop, only a **question**.

This is where the abstraction leaks. A team started using the platform to avoid seeing its own
infrastructure layer; now four items sit on its bill, and changing their amount means learning that
layer anyway. **Showing an item nobody controls is not visibility — it is noise.**

## Central Accounting's Threshold

The fourth table asks whether the platform pays off at all. The central accounting pipeline costs a
fixed 120 units a day; a team standing up its own accounting pays 41 units. The threshold is **three
teams**: at one team, the central pipeline is 79 units **more expensive**; at two teams, 38 units more
expensive. With few teams, platform accounting does not pay for itself, and the table shows this is
arithmetic, not a preference.

At five teams, the gross gain is 85 units. Onto that come the ones who opt out: reporting (−39.7) and
field (+16.1), the two that deviate most under the usage rule, do not trust the result and stand up
their own measurement. Their duplicated measurement costs **82 units**; the gross 85-unit gain drops to
**3 units**. Five teams run central accounting at a point where it almost pays nothing, and the reason
is not the cost itself — it is **distrust of the allocation rule**.

## Summary

- Only 47.5% of a daily 1,376 fictional units is directly assigned; 38.1% is shared, 14.4% is
  unallocatable — **52.5%** of the bill cannot be written to a team without a rule.
- The three allocation rules mislead three different teams in opposite directions: equal share loads
  field by +79.0, request count loads meter reading by +140.7 and lets reporting off by −93.6; even
  usage ratio, the least wrong rule, has an absolute deviation of 89.8 units.
- The 198-unit unallocatable item is spread out under every rule, but its deviation cannot be measured;
  a wrong that is tolerable under showback comes out of a budget under chargeback.
- The bill drops 15.6% once cost is shown; the highest response is in temporary environment copies
  (55%). Four items never move at all — 408 units, 29.7% — because they are not under a team's control.
- Central accounting pays for itself after three teams; at two teams it is 38 units more expensive. At
  five teams the gross gain is 85 units, but two teams that do not trust the rule standing up their own
  measurement drags the gain down to 3 units.

## Next Step

This lesson's numbers say how much the platform costs, but not who it benefits. The next lesson spreads
the question out over time: the five teams do not move to the new arrangement all at once, they move in
waves, and the wave's size sets both how long the two arrangements run side by side and how many are
left behind. The cost to a team left behind is a line item that never appears in this lesson's table at
all.
