Lesson 05 / 12
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.
Published Updated
In this lesson
- 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
Contents
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.
// 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.
// 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 changeproduces 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.