---
title: 'Budget and Alerts'
source: 'https://academia.sh/en/courses/cloud-governance/budget-and-alerts'
course: 'Cloud Governance, Security and Cost'
language: en
updated: '2026-09-13T05:07:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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"
---

# 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.

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.

```js
// 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);
```

```js
// 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.
