Lesson 18 / 21
Budget and Alerts
Measures when unexpected spending gets noticed: across five rises placed on a fictional spending curve, the threshold alert misses two of them entirely and, by the time it first fires, 86.4% of the monthly budget is already spent (204 units remain); the forecast alert catches all five but stays open on 63 of ninety days. The threshold family's best setting misses 821 units while the forecast family stays at 12.5; the false-alarm gap is 5 versus 6 across 21 event-free days. The bill's delay adds directly to the day noticed: a three-day delay adds 1.8 days to the average delay and 210 units to the missed amount, and an eight-day delay lets one rise go entirely unnoticed.
Published Updated
In this lesson
- Runs two alert kinds over a spending curve: a threshold alert that watches the budget percentage and a forecast alert that predicts month end
- Counts, for every rise, the day it is noticed, the rises that get missed, and the false-alarm count, each separately
- Measures the bill's delay as a constraint: how late the alert arrives and how much of the budget is spent by those days
Contents
The previous two lessons split the bill into items and made one attempt at ownership; both happen after the bill arrives. When the month closes, which team spent how much is known, and the spending is already done.
This lesson’s question sits inside the month: when spending rises unexpectedly, which day it gets noticed, and whether a rise ever ends unnoticed.
The Spending Curve and Two Alerts
The example is the fictional regional measurement network: five teams, three environments, a
rented machine, network, store, function, and edge layer. The curve, budget, and events are
fictional; the currency is fictional too and is called unit, corresponding to no real price
list.
CC13: the monthly budget is 1,500 units and three months are watched. CC14: the curve is built from a baseline, a weekly pattern, slow growth, and noise; growth alone drifts the event-free baseline toward the budget too. CC15: five rises are placed on the curve; four of them end in the month they start. CC16: the threshold alert watches the month’s cumulative spend against the budget ratio; the forecast alert predicts month end from the last k days’ average. CC17: every day the condition holds is an alert day; one that arrives during a rise is a notice, one on an event-free day is a false alarm. CC18: the bill arrives delayed — day g‘s decision is made with g−delay’s data, so the condition shifts by exactly the delay.
The curve, budget, and alerts are a process-internal model; there is no real spending record.
// cost/spend.mjs — daily spend curve model: no real spend record. The curve is made of a baseline, // a weekly pattern, slow growth, and hand-placed *events*; noise comes from a generator, the seed // is visible, the currency is fictional and is called `unit`. The event-free baseline is also // computed separately: an event's *excess* spend is the difference between the two. export const SEED = 3391; export const MONTHS = 3, MONTH_DAYS = 30, DAYS = MONTHS * MONTH_DAYS; export const BUDGET = 1500; // monthly budget (unit, fictional) export const BASE = 43, GROWTH = 0.12; // daily baseline and daily slow growth export const WEEK = [1.08, 1.10, 1.06, 1.04, 0.98, 0.72, 0.68]; // event: name, start day (1..90), duration (days), daily excess unit export const EVENT = [ ["dev environment left running", 9, 22, 12.5], ["temporary data copy", 38, 6, 41.0], ["wrong size choice", 47, 40, 9.0], ["edge node duplication", 63, 25, 7.5], ["reprocessing loop", 71, 4, 63.0], ]; export const active = (d) => EVENT.filter(([, b, s]) => d >= b && d < b + s); export const series = (withEvents = true) => { let s = SEED >>> 0; const r = () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; const v = [0]; for (let d = 1; d <= DAYS; d++) { const noise = 0.94 + r() * 0.12; let x = (BASE + GROWTH * (d - 1)) * WEEK[(d - 1) % 7] * noise; if (withEvents) for (const [, , , f] of active(d)) x += f; v.push(Math.round(x * 100) / 100); } return v; }; export const month = (d) => Math.floor((d - 1) / MONTH_DAYS); export const dayOfMonth = (d) => ((d - 1) % MONTH_DAYS) + 1; export const sum = (v) => v.reduce((a, b) => a + b, 0);
// cost/alert.mjs — two alert kinds run on the same curve: threshold watches budget percentage, // forecast predicts month end from the last *k* days' average. What is counted is not correctness // but *the day noticed* — how many days after an event starts the alert fires, whether the rise // ever goes unnoticed. The bill's delay is a constraint: day g's decision uses g-DELAY's data, so // the condition shifts by exactly DELAY days. Every day the condition holds is an *alert day*. import { EVENT, BUDGET, MONTHS, MONTH_DAYS, DAYS, SEED, active, series, month, dayOfMonth, sum } from "./spend.mjs"; const write = (w, ...s) => console.log(s.map((v, i) => (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join("")); const t0 = process.hrtime.bigint(); const ACTUAL = series(), BASELINE = series(false); const excess = (d) => ACTUAL[d] - BASELINE[d]; const monthTotal = (v, m) => sum(v.slice(m * MONTH_DAYS + 1, (m + 1) * MONTH_DAYS + 1)); const cumulative = (m, d) => sum(ACTUAL.slice(m * MONTH_DAYS + 1, m * MONTH_DAYS + d + 1)); console.log(`seed ${SEED}: ${DAYS} days, monthly budget ${BUDGET} units (fictional currency)`); console.log("\n1. spending curve: budget, actual, overrun"); const A = [7, 10, 14, 11, 11, 14]; write(A, "month", "budget", "actual", "overrun", "baseline", "event excess"); for (let m = 0; m < MONTHS; m++) { const act = monthTotal(ACTUAL, m), base = monthTotal(BASELINE, m); write(A, m + 1, BUDGET, act.toFixed(1), (act - BUDGET).toFixed(1), base.toFixed(1), (act - base).toFixed(1)); } const withinMonth = EVENT.filter(([, b, s2]) => month(b) === month(b + s2 - 1)).length; console.log(` ${EVENT.length} events placed (in the model above); ${withinMonth} of them ` + `end in the month they start`); // run one alert kind over the curve; returns the list of triggered days const simulate = (kind, p, delay) => { const days = []; for (let g = 1; g <= DAYS; g++) { const v = g - delay; // last day readable from the bill if (v < 1) continue; const m = month(v), d = dayOfMonth(v); if (month(g) !== m) continue; // month boundary: decide with the month's own data const acc = cumulative(m, d); let trigger = false; if (kind === "threshold") trigger = acc / BUDGET >= p; else { if (d < p) continue; const avg = sum(ACTUAL.slice(m * MONTH_DAYS + d - p + 1, m * MONTH_DAYS + d + 1)) / p; trigger = acc + avg * (MONTH_DAYS - d) > BUDGET; } if (trigger) days.push(g); } return days; }; // map alert days onto events: day noticed, delay, excess spent during the delay const measure = (days) => { const found = EVENT.map(([, b, s]) => { const g = days.find((x) => x >= b && x < b + s); return g === undefined ? null : g; }); const falseAlarms = days.filter((g) => active(g).length === 0).length; const missed = EVENT.filter((_, i) => found[i] === null).length; const delays = found.map((g, i) => (g === null ? null : g - EVENT[i][1])); const missedUnits = sum(EVENT.map(([, b, s], i) => { const end = found[i] === null ? b + s : found[i]; let x = 0; for (let g = b; g < end; g++) x += excess(g); return x; })); const delayVals = delays.filter((x) => x !== null); return { found, delays, falseAlarms, missed, missedUnits, total: days.length, avgDelay: delayVals.length ? sum(delayVals) / delayVals.length : NaN }; }; console.log("\n2. two alert kinds, same curve (threshold 85%, forecast window 7 days, delay 0)"); const es = simulate("threshold", 0.85, 0), ta = simulate("forecast", 7, 0); const measuredThreshold = measure(es), measuredForecast = measure(ta); const C = [-32, 9, 15, 10, 14, 10]; write(C, "event", "starts", "threshold day", "delay", "forecast day", "delay"); for (let i = 0; i < EVENT.length; i++) { const g = (x) => (x === null ? "missed" : x); write(C, EVENT[i][0], EVENT[i][1], g(measuredThreshold.found[i]), g(measuredThreshold.delays[i]), g(measuredForecast.found[i]), g(measuredForecast.delays[i])); } // how much of the budget is spent by the day the alert arrives: each month's first alert day const spentShare = (days) => { const first = []; for (let m = 0; m < MONTHS; m++) { const g = days.find((x) => month(x) === m); if (g !== undefined) first.push(cumulative(m, dayOfMonth(g)) / BUDGET); } return sum(first) / first.length; }; for (const [name, o, g] of [["threshold", measuredThreshold, es], ["forecast", measuredForecast, ta]]) { console.log(` ${name.padEnd(9)} ${o.total} alert days, ${EVENT.length - o.missed} events ` + `caught, ${o.missed} missed, ${o.falseAlarms} false-alarm days`); console.log(` ${" ".repeat(9)} ${o.missedUnits.toFixed(1)} units excess during the delay; first alert ` + `day ${(100 * spentShare(g)).toFixed(1)}% of the budget is spent, ` + `${(BUDGET * (1 - spentShare(g))).toFixed(0)} units remain`); } console.log("\n3. parameter scan: how early the alert fires, how many false alarms"); const D = [-11, 9, 12, 10, 8, 14, 8, 14]; write(D, "kind", "setting", "alert days", "caught", "missed", "avg delay", "false", "missed units"); const settings = [["threshold", 0.60], ["threshold", 0.85], ["threshold", 1.00], ["forecast", 3], ["forecast", 7], ["forecast", 14]]; const scored = []; for (const [kind, p] of settings) { const o = measure(simulate(kind, p, 0)); scored.push([kind, p, o]); write(D, kind, kind === "threshold" ? `${(100 * p).toFixed(0)}%` : `${p} days`, o.total, EVENT.length - o.missed, o.missed, isNaN(o.avgDelay) ? "-" : o.avgDelay.toFixed(1), o.falseAlarms, o.missedUnits.toFixed(1)); } const best = scored.reduce((a, b) => (b[2].missedUnits < a[2].missedUnits ? b : a)); console.log(` least-missing setting: ${best[0]} ${best[1]}, ` + `${best[2].missedUnits.toFixed(1)} units, ${best[2].falseAlarms} false-alarm days`); const eventFree = [...Array(DAYS)].map((_, i) => i + 1).filter((g) => active(g).length === 0).length; console.log(` event-free days ${eventFree}/${DAYS}: a false alarm can only happen on these days`); console.log(` ${best[0]} ${best[1]} keeps the alert open on ${best[2].total} of ${DAYS} days ` + `(${(100 * best[2].total / DAYS).toFixed(0)}%)`); console.log("\n4. the bill's delay is a constraint: the decision is made with g-DELAY's data"); const F = [9, 12, 8, 14, 14, 14]; write(F, "delay", "alert days", "missed", "avg delay", "missed units", "budget share"); for (const gk of [0, 1, 3, 5, 8]) { const o = measure(simulate(best[0], best[1], gk)); write(F, `${gk} day${gk === 1 ? "" : "s"}`, o.total, o.missed, isNaN(o.avgDelay) ? "-" : o.avgDelay.toFixed(1), o.missedUnits.toFixed(1), `${(100 * o.missedUnits / (MONTHS * BUDGET)).toFixed(1)}%`); } const g0 = measure(simulate(best[0], best[1], 0)), g3 = measure(simulate(best[0], best[1], 3)); console.log(` a 3-day delay adds ${(g3.avgDelay - g0.avgDelay).toFixed(1)} days to the day ` + `noticed and grows the missed amount by ${(g3.missedUnits - g0.missedUnits).toFixed(1)} units`); const ms = Number(process.hrtime.bigint() - t0) / 1e6; // raw measurement is not printed, only the band is checked console.log(`\nfour tables within the 2000 ms band: ${ms < 2000 ? "yes" : "no"}`);
seed 3391: 90 days, monthly budget 1500 units (fictional currency)
1. spending curve: budget, actual, overrun
month budget actual overrun baseline event excess
1 1500 1553.7 53.7 1278.7 275.0
2 1500 1752.8 252.8 1380.8 372.0
3 1500 2137.0 637.0 1463.5 673.5
5 events placed (in the model above); 4 of them end in the month they start
2. two alert kinds, same curve (threshold 85%, forecast window 7 days, delay 0)
event starts threshold day delay forecast day delay
dev environment left running 9 25 16 15 6
temporary data copy 38 missed missed 38 0
wrong size choice 47 52 5 47 0
edge node duplication 63 77 14 67 4
reprocessing loop 71 missed missed 71 0
threshold 29 alert days, 3 events caught, 2 missed, 3 false-alarm days
1292.0 units excess during the delay; first alert day 86.4% of the budget is spent, 204 units remain
forecast 63 alert days, 5 events caught, 0 missed, 6 false-alarm days
141.0 units excess during the delay; first alert day 34.6% of the budget is spent, 982 units remain
3. parameter scan: how early the alert fires, how many false alarms
kind setting alert days caught missed avg delay false missed units
threshold 60% 46 4 1 5.5 5 821.0
threshold 85% 29 3 2 11.7 3 1292.0
threshold 100% 17 3 2 15.7 3 1440.0
forecast 3 days 68 5 0 0.2 6 12.5
forecast 7 days 63 5 0 2.0 6 141.0
forecast 14 days 47 4 1 5.8 6 967.5
least-missing setting: forecast 3, 12.5 units, 6 false-alarm days
event-free days 21/90: a false alarm can only happen on these days
forecast 3 keeps the alert open on 68 of 90 days (76%)
4. the bill's delay is a constraint: the decision is made with g-DELAY's data
delay alert days missed avg delay missed units budget share
0 days 68 0 0.2 12.5 0.3%
1 day 65 0 0.8 82.5 1.8%
3 days 59 0 2.0 222.5 4.9%
5 days 53 0 3.2 362.5 8.1%
8 days 44 1 4.3 490.5 10.9%
a 3-day delay adds 1.8 days to the day noticed and grows the missed amount by 210.0 units
four tables within the 2000 ms band: yes
The numbers belong to the measurement class; their inputs are the assumptions above.
The Threshold Alert Waits on the Budget
The first table shows all three months overrun: 53.7 — 252.8 — 637.0 units. The source is not only the rises; the event-free baseline climbs from 1,278.7 to 1,463.5, filling 97.6% of the budget in the third month.
The second table runs the threshold alert at 85%. Two rises go entirely unnoticed: the temporary data copy (6 days, 41 units a day) and the reprocessing loop (4 days, 63 units a day). Both short and large, they end before their cumulative total clears the threshold. The long, small ones are noticed 16 and 14 days later; the total missed is 1,292 units.
The reason is the measure itself: the threshold alert watches cumulative spend, not the rise. By the day it first fires, an average 86.4% of the budget is already spent, leaving 204 units — a notice of what remains, not a warning. Even at 60% the missed amount is 821 units, and one short rise still goes unnoticed.
The Forecast Alert Is Early but Constant
At a seven-day window, the forecast alert catches all five rises, noticing two short ones the day they start (38 and 71), because the last seven days’ average jumps in a day and the month-end forecast clears the budget. The missed amount drops from 1,292 to 141 units, and only 34.6% of the budget is spent by the first alert — 982 units remain.
The third table shows the price. Shrinking the window to three days drops the missed amount to 12.5 units but keeps the alert open 68 of 90 days (76%). False alarms do not explain it: 6 of 21 event-free days carry one, against 5 at a 60% threshold. What separates the two families is not false alarms but how long the true alarm rings: the forecast alert sounds for as long as the rise lasts and stops being distinctive, while the threshold stays quiet but speaks late.
Widening the window to fourteen days makes the forecast alert behave like the threshold: one rise goes unnoticed, and the missed amount climbs to 967.5 units.
The Bill’s Delay Sets a Ceiling
The fourth table adds one constraint: the bill’s data arrives delayed. Because the alert condition shifts by exactly the delay, the day noticed shifts the same amount. Average delay climbs from 0.2 days to 0.8 at one day, 2.0 at three, 4.3 at eight.
The missed amount tracks this linearly: 12.5 — 82.5 — 222.5 — 362.5 — 490.5 units. At an eight-day delay, the four-day reprocessing loop ends before its data ever appears and goes unnoticed; 10.9% of the three-month budget is spent unnoticed.
This is a ceiling: however much the alert setting improves, the day noticed cannot precede the bill’s delay. The delay adds no separate cost item either — the amount leaving the budget is the same, only spent before the alert arrives.
Summary
- All three months overrun (53.7 — 252.8 — 637.0 units); part of it comes not from a rise but from baseline growth — the event-free baseline fills 97.6% of the budget in the third month.
- The threshold alert watches cumulative spend, not the rise: at an 85% threshold, two short rises go entirely unnoticed, and by its first firing 86.4% of the budget is already spent — 204 units remain. Total missed: 1,292 units.
- The forecast alert catches all five rises at a seven-day window and drops the missed amount to 141 units; 982 units remain by the first alert.
- The cost of noticing early is not false alarms but duration: a three-day window drops the missed amount to 12.5 units but keeps the alert open 68 of 90 days (76%); false alarms sit at 6 of 21 event-free days, against 5 at 60%.
- The bill’s delay adds directly to the day noticed: a 3-day delay adds 1.8 days to the average and 210 units to the missed amount; at an 8-day delay a four-day rise goes entirely unnoticed and 10.9% of the three-month budget is spent unnoticed.
Next Step
An alert says spending rose unexpectedly; it says nothing about expected spending. As the baseline edges closer to the budget every month, no alert fires — there is no event, only resources built larger than needed. The next lesson looks at that baseline: downsizing candidates are drawn from usage history, and each candidate’s savings and risk are counted together.
To keep your progress and take notes, Log in
My notes
Log in to take notes.