---
title: Self-Service
source: 'https://academia.sh/en/courses/platform-engineering/self-service'
course: 'Platform Engineering'
language: en
updated: '2026-09-13T05:08:29+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Runs the same request set through a request queue and measures wait time and the platform team's interruptions in person-days"
    - 'Counts the cost of self-service through interface writing, maintenance, misuse, and irreversible-operation items'
    - 'Derives the per-type threshold and names the request types where automation does not pay off'
---

# Self-Service

Measuring what removing the request queue is worth: the same request set is run through a request queue and through a self-service regime, wait time and the platform team's interruptions are counted, the costs of writing the interface, misuse, and irreversible operations are derived, and the request type where automation does not pay off is named.

Across four lessons, the platform was measured as a cost calculation, an operating model, a
scope, and a mode choice. All four shared one assumption: when a team wants something, **someone
on the platform team does it**. Changing a default option, entering the path, getting a
capability — all of it passed through a person's hands. That person stands behind a queue.

This lesson measures the queue and tries to remove it. Self-service takes the request out of the
platform team's hands and puts it in the hands of the team that opens it. What it gains is wait
time and interruptions; what it costs is the work of writing the interface, the interface's
maintenance, misuse, and **irreversible operations**. The numbers also show where automation does
not pay off. The through-line is the regional measurement network and its five teams; both the
network and the teams are fictional.

## The Request Queue

**PA25 — there are twelve request types; each is written with how many times a year it arrives,
how many hours a manual fulfillment takes, and its variability.** If variability is zero, the
request is identical every time; near one, it is different every time. The reason: whether a
request can be automated does not come from its frequency, it comes from how much it changes from
one occurrence to the next; the threshold cannot be derived without separating the two numbers.

**PA26 — in the request queue, a request goes to the platform team; the team sets aside 4 hours a
day for requests and works through them in order. Every request adds 0.25 hours of interruption
on top of its manual work.** The reason: the cost of manual work is not just the time the work
itself takes; the team steps out of another task, handles the request, and has to rebuild the
context of the task it returns to. The window is three years, a year is 250 workdays, a workday
is 8 hours.

```js
// queue.mjs — the same request set is run through a request queue (model)
import { writeFileSync } from "node:fs";

// PA25 — twelve request types: name, how many times a year it arrives, the hours a manual fulfillment
// takes, and variability (0 = identical every time, 1 = different every time). Numbers are fictional.
const TYPE = [
  ["pipeline variable change", 150, 0.3, 0.05], ["privilege grant", 120, 0.4, 0.05],
  ["secret addition", 90, 0.5, 0.05], ["new environment setup", 40, 3, 0.10],
  ["dashboard panel addition", 30, 2, 0.30], ["database copy", 24, 4, 0.20],
  ["domain name binding", 20, 1.5, 0.15], ["new service scaffold", 18, 6, 0.25],
  ["audit log export", 12, 2.5, 0.40], ["restore from backup", 6, 8, 0.60],
  ["cluster version upgrade", 4, 20, 0.50], ["region migration", 3, 40, 0.90],
].map(([name, perYear, hours, variability]) => ({ name, perYear, hours, variability }));

// PA26 — request queue: a request goes to the platform team, which sets aside 4 hours a day for requests and
// works through them in order. Every request adds a 0.25-hour interruption on top of its manual work: the
// team steps out of another task and back into the request. Window is three years, a year is 250 workdays, a workday is 8 hours.
const DAILY = 4, YEAR = 250, WINDOW = 3, INTERRUPTION = 0.25, DAY_HOURS = 8;

function queue(scale) {
  const arrivals = TYPE.flatMap((t, i) => {
    const n = t.perYear * WINDOW, work = t.hours * (scale ? scale(t) : 1);
    return Array.from({ length: n }, (_, j) => ({ type: i, day: (j + 0.5) * YEAR * WINDOW / n,
      work: work + INTERRUPTION }));
  }).sort((a, b) => a.day - b.day);
  let free = 0, total = 0;
  for (const g of arrivals) {
    free = Math.max(free, g.day * DAILY) + g.work;
    g.wait = free / DAILY - g.day;
    total += g.work;
  }
  const w = arrivals.map((g) => g.wait).sort((x, y) => x - y);
  return { arrivals, count: arrivals.length, total,
    avg: w.reduce((x, y) => x + y, 0) / w.length, median: w[w.length >> 1], longest: w.at(-1),
    utilization: total / (YEAR * WINDOW * DAILY) };
}
const Q = queue(null);
writeFileSync("queue.json", JSON.stringify({ TYPE, baseline: {
  avg: Q.avg, median: Q.median, longest: Q.longest, total: Q.total, count: Q.count } }));

console.log("type".padEnd(26) + "per year".padStart(9) + "hours".padStart(6) +
  "variability".padStart(13) + "3-yr requests".padStart(14) + "manual work (days)".padStart(19) +
  "avg. wait (days)".padStart(20));
for (let i = 0; i < TYPE.length; i++) {
  const g = Q.arrivals.filter((x) => x.type === i);
  console.log(TYPE[i].name.padEnd(26) + `${TYPE[i].perYear}`.padStart(9) +
    `${TYPE[i].hours}`.padStart(6) + `${TYPE[i].variability.toFixed(2)}`.padStart(13) +
    `${g.length}`.padStart(14) +
    `${(g.length * TYPE[i].hours / DAY_HOURS).toFixed(1)}`.padStart(19) +
    `${(g.reduce((t, x) => t + x.wait, 0) / g.length).toFixed(1)}`.padStart(20));
}
const interruptionDays = Q.count * INTERRUPTION / DAY_HOURS;
console.log(`\nover three years, ${Q.count} requests, ${(Q.total / DAY_HOURS).toFixed(1)} person-days ` +
  `(of which ${interruptionDays.toFixed(1)} person-days are interruption alone), utilization ` +
  `${(100 * Q.utilization).toFixed(1)}%`);
console.log(`wait: average ${Q.avg.toFixed(1)} days, median ${Q.median.toFixed(1)} days, ` +
  `longest ${Q.longest.toFixed(1)} days`);
```

```
type                       per year hours  variability 3-yr requests manual work (days)    avg. wait (days)
pipeline variable change        150   0.3         0.05           450               16.9                 4.8
privilege grant                 120   0.4         0.05           360               18.0                 4.7
secret addition                  90   0.5         0.05           270               16.9                 4.7
new environment setup            40     3         0.10           120               45.0                 5.3
dashboard panel addition         30     2         0.30            90               22.5                 5.0
database copy                    24     4         0.20            72               36.0                 5.6
domain name binding              20   1.5         0.15            60               11.3                 4.5
new service scaffold             18     6         0.25            54               40.5                 5.7
audit log export                 12   2.5         0.40            36               11.3                 4.4
restore from backup               6     8         0.60            18               18.0                 7.4
cluster version upgrade           4    20         0.50            12               30.0                 8.4
region migration                  3    40         0.90             9               45.0                11.1

over three years, 1551 requests, 359.7 person-days (of which 48.5 person-days are interruption alone), utilization 95.9%
wait: average 5.0 days, median 4.5 days, longest 13.0 days
```

Over three years, the queue sees **1,551 requests** and spends **359.7 person-days**. Of that,
**48.5 person-days** advance no request at all: purely the cost of context switching, **13.5%**
of the work spent. Utilization is **95.9%**, and that figure explains why the wait runs this
long: average wait is **5 days**, the longest is **13 days**.

Wait does not spread evenly across types. A pipeline variable change waits 4.8 days on average, a
forty-hour region migration waits 11.1 days; small jobs wait behind large ones. The median (4.5)
sitting below the average (5.0) shows this — a handful of long jobs pull the distribution
upward.

## The Cost of Self-Service

**PA27 — for a type, building an interface costs 4 + 20 × variability person-days, its yearly
maintenance is a fifth of that cost, and the interface covers only (1 − variability) of
requests; the rest is still done manually.** The reason: putting a request that differs every
time behind an interface only captures the part of it that does not change.

**PA28 — under self-service, 0.02 + 0.10 × variability of requests are misused; 85% of those are
reversible (0.5-person-day fix), 15% are irreversible (6-person-day recovery).** The reason:
whoever is performing the operation behind the interface is doing it for the first time, and no
one is looking over their shoulder. **PA29 — an interface for a type pays for itself if its
three-year-window total does not exceed the manual regime's total.** **PA30 — the mixed regime
writes an interface only for types that pay for themselves; the rest, plus the manual share of
what is automated, stay in the queue.**

```js
// service.mjs — the cost of self-service, per-type threshold, and the queue in the mixed regime
import { readFileSync } from "node:fs";
const { TYPE, baseline } = JSON.parse(readFileSync("queue.json", "utf8"));

// PA27 — for a type, building an interface costs 4 + 20 x variability person-days, yearly
// maintenance is a fifth of that cost, and the interface covers only (1 - variability) of
// requests; the rest is still done manually. PA28 — under self-service, 0.02 + 0.10 x
// variability of requests are misused; 85% of those are reversible (0.5 person-day fix),
// 15% are irreversible (6 person-day recovery). PA29 — an interface for a type pays for
// itself if its three-year-window total does not exceed the manual regime's total.
const DAILY = 4, YEAR = 250, WINDOW = 3, INTERRUPTION = 0.25, DAY_HOURS = 8;
const REVERSIBLE = 0.5, IRREVERSIBLE = 6;

const calc = TYPE.map((t) => {
  const n = t.perYear * WINDOW, manual = n * (t.hours + INTERRUPTION) / DAY_HOURS;
  const interfaceCost = 4 + 20 * t.variability, maintenance = WINDOW * interfaceCost / 5;
  const misuse = n * (0.02 + 0.10 * t.variability);
  const damage = misuse * 0.85 * REVERSIBLE + misuse * 0.15 * IRREVERSIBLE;
  const selfService = interfaceCost + maintenance + t.variability * manual + damage;
  return { ...t, n, manual, interfaceCost, maintenance, misuse, damage, selfService, diff: manual - selfService };
});

console.log("type".padEnd(26) + "manual".padStart(7) + "interface".padStart(10) + "maint.".padStart(7) +
  "manual left".padStart(13) + "misuse".padStart(8) + "irreversible".padStart(15) +
  "damage".padStart(8) + "self-service".padStart(13) + "diff".padStart(8));
for (const h of calc)
  console.log(h.name.padEnd(26) + h.manual.toFixed(1).padStart(7) + h.interfaceCost.toFixed(1).padStart(10) +
    h.maintenance.toFixed(1).padStart(7) + (h.variability * h.manual).toFixed(1).padStart(13) +
    h.misuse.toFixed(1).padStart(8) + (h.misuse * 0.15).toFixed(2).padStart(15) +
    h.damage.toFixed(1).padStart(8) + h.selfService.toFixed(1).padStart(13) +
    ((h.diff >= 0 ? "+" : "") + h.diff.toFixed(1)).padStart(8));
const payoff = calc.filter((h) => h.diff > 0), noPayoff = calc.filter((h) => h.diff <= 0);
console.log(`\ntypes that pay for themselves: ${payoff.length} / ${TYPE.length}, ` +
  `total gain ${payoff.reduce((t, h) => t + h.diff, 0).toFixed(1)} person-days`);
console.log("the ones that do not — " + noPayoff.map((h) => `${h.name} (${h.diff.toFixed(1)})`).join(", "));

// PA30 — mixed regime: an interface is written only for types that pay for themselves; the
// rest, plus the manual share of the ones automated, stay in the queue. The queue is run again.
const queue = (scale) => {
  const arrivals = TYPE.flatMap((t, i) => {
    const n = Math.round(t.perYear * WINDOW * scale(calc[i]));
    return Array.from({ length: n }, (_, j) => ({ day: (j + 0.5) * YEAR * WINDOW / n,
      work: t.hours + INTERRUPTION }));
  }).sort((a, b) => a.day - b.day);
  let free = 0, total = 0;
  for (const g of arrivals) {
    free = Math.max(free, g.day * DAILY) + g.work;
    g.wait = free / DAILY - g.day;
    total += g.work;
  }
  const w = arrivals.map((g) => g.wait).sort((x, y) => x - y);
  return { count: arrivals.length, days: total / DAY_HOURS,
    avg: w.reduce((x, y) => x + y, 0) / w.length, longest: w.at(-1),
    utilization: total / (YEAR * WINDOW * DAILY) };
};
const MIXED = queue((h) => (h.diff > 0 ? h.variability : 1));
const ALL_AUTOMATED = queue((h) => h.variability);
console.log("\n" + "regime".padEnd(30) + "entering queue".padStart(15) + "manual work".padStart(12) +
  "utilization".padStart(12) + "avg. wait".padStart(14) + "longest".padStart(9));
console.log("request queue (all manual)".padEnd(30) + `${baseline.count}`.padStart(15) +
  (baseline.total / DAY_HOURS).toFixed(1).padStart(12) +
  `${(100 * baseline.total / (YEAR * WINDOW * DAILY)).toFixed(1)}%`.padStart(12) +
  baseline.avg.toFixed(1).padStart(14) + baseline.longest.toFixed(1).padStart(9));
for (const [name, K] of [["mixed (interface for 6 types)", MIXED], ["interface for all", ALL_AUTOMATED]])
  console.log(name.padEnd(30) + `${K.count}`.padStart(15) + K.days.toFixed(1).padStart(12) +
    `${(100 * K.utilization).toFixed(1)}%`.padStart(12) + K.avg.toFixed(1).padStart(14) +
    K.longest.toFixed(1).padStart(9));
const interfaceWork = (H) => H.reduce((t, h) => t + h.interfaceCost + h.maintenance + h.damage, 0);
console.log(`\ninterface + maintenance + damage: mixed regime ${interfaceWork(payoff).toFixed(1)}, ` +
  `all types ${interfaceWork(calc).toFixed(1)} person-days`);
```

```
type                       manual interface maint.  manual left  misuse   irreversible  damage self-service    diff
pipeline variable change     30.9       5.0    3.0          1.5    11.3           1.69    14.9         24.5    +6.5
privilege grant              29.3       5.0    3.0          1.5     9.0           1.35    11.9         21.4    +7.9
secret addition              25.3       5.0    3.0          1.3     6.8           1.01     8.9         18.2    +7.1
new environment setup        48.8       6.0    3.6          4.9     3.6           0.54     4.8         19.2   +29.5
dashboard panel addition     25.3      10.0    6.0          7.6     4.5           0.67     6.0         29.6    -4.2
database copy                38.3       8.0    4.8          7.7     2.9           0.43     3.8         24.3   +14.0
domain name binding          13.1       7.0    4.2          2.0     2.1           0.32     2.8         16.0    -2.8
new service scaffold         42.2       9.0    5.4         10.5     2.4           0.36     3.2         28.2   +14.0
audit log export             12.4      12.0    7.2          5.0     2.2           0.32     2.9         27.0   -14.6
restore from backup          18.6      16.0    9.6         11.1     1.4           0.22     1.9         38.6   -20.1
cluster version upgrade      30.4      14.0    8.4         15.2     0.8           0.13     1.1         38.7    -8.3
region migration             45.3      22.0   13.2         40.8     1.0           0.15     1.3         77.3   -32.0

types that pay for themselves: 6 / 12, total gain 79.0 person-days
the ones that do not — dashboard panel addition (-4.2), domain name binding (-2.8), audit log export (-14.6), restore from backup (-20.1), cluster version upgrade (-8.3), region migration (-32.0)

regime                         entering queue manual work utilization     avg. wait  longest
request queue (all manual)               1551       359.7       95.9%           5.0     13.0
mixed (interface for 6 types)             320       172.6       46.0%           2.1     10.3
interface for all                         170       108.8       29.0%           2.3     10.1

interface + maintenance + damage: mixed regime 108.4, all types 253.9 person-days
```

## Where Automation Does Not Pay Off

**Six** of the twelve types pay for themselves, for a total gain of **79 person-days**. Six do
not, and the end of that list is the situation this lesson takes its name from: **`region
migration` — a request that arrives three times a year and is different every time.** Doing it
manually costs 45.3 person-days over three years; putting it behind an interface costs **77.3
person-days**, **32 person-days more**. The math comes from three items: the interface at 22
person-days, three years of maintenance at 13.2 person-days, and — the decisive one — since the
interface covers only a tenth of the work, **40.8 person-days of manual work still remain in
place**. The same pattern repeats in `restore from backup` and `cluster version upgrade`.

A second cost sits in the middle of the table. In the `pipeline variable change` type, the
interface pays for itself, but its gain is only 6.5 person-days; over three years it produces
**11.3 instances of misuse**, and **1.69 of them are irreversible** — 14.9 person-days of damage
in total, more than the combined interface and maintenance cost (8 person-days). **The most
frequent request is the one whose interface gets misused the most**, because misuse count scales
with frequency. This item did not exist at all in the manual regime: a person looked at the
operation before performing it.

The last table shows what became of the queue. In the mixed regime, requests entering the queue
drop **from 1,551 to 320**, manual work **from 359.7 to 172.6 person-days**, and utilization
**from 95.9% to 46%**; average wait falls **from 5.0 days to 2.1 days**. **Automating six types
also shortens the wait for the six types that remain in the queue** — because the queue has
drained.

Automating everything does not go any further than this. Requests entering the queue drop to 170,
manual work to 108.8 person-days, but average wait **rises to 2.3 days**: what remains in the
queue is now only long, variable work, and a single region migration closes the queue for days at
a time. The interface, maintenance, and damage paid for this comes to **253.9 person-days**, more
than double the mixed regime's **108.4 person-days**. Removing the queue entirely is **145
person-days more expensive** than shortening it, and gives nothing back in wait.

## Summary

- Twelve request types entered the queue over a three-year window: 1,551 requests, 359.7
  person-days of manual work, of which 48.5 person-days (13.5%) are interruption alone;
  utilization 95.9%, average wait 5.0 days, longest 13.0 days.
- Wait does not spread evenly across types: pipeline variable change waits 4.8 days, region
  migration waits 11.1 days; the median (4.5) sits below the average (5.0).
- Six of the twelve types pay for themselves and gain 79 person-days; six do not.
- The type where automation does not pay off is `region migration`: three times a year,
  different every time. Manual costs 45.3 person-days, an interface costs 77.3 — a 32-person-day
  loss, because 40.8 person-days of manual work stay in place.
- Self-service's second cost is misuse: `pipeline variable change` produces 11.3 instances of
  misuse over three years, 1.69 of them irreversible, 14.9 person-days total — larger than the
  combined interface and maintenance cost.
- In the mixed regime, requests entering the queue drop from 1,551 to 320, utilization from 95.9%
  to 46%, average wait from 5.0 to 2.1 days. Automating everything raises wait to 2.3 days and
  costs 145 person-days more.

## Next Step

Across these five lessons, the platform was defined as an approach and its boundaries were
drawn: the team count past which it pays for itself, the adoption and dead-capability counts of
two operating models, the path's scope and the loss for those left outside, mandate's three
modes, the cost of the queue, and where self-service does not pay off. All of them were numbers
about **how** the platform would be run.

What went unsaid is **what the platform offers**. Across five lessons, the names "capability,"
"item," "request type" appeared, but none of them was opened up: how an environment arrives on
demand, what scaffolding does and does not generate, how a catalog keeps track of ownership, how
policy gets embedded in the pipeline, how cost gets allocated, how migration proceeds, and by
what measure the platform's effect gets counted. The next topic opens these capabilities one by
one and counts each with the same measure — the layer it hides, the part that leaks back
through, and who steps outside it.
