Outcome
By the end of this lesson, you will be able to defend the architectural choice to compute budget spend live (rather than store a current_spend_usd column), predict the performance characteristics, and reason about why this choice matches the two-source cost model.
| Prerequisites | M4.6.L1-L4 (budget basics + signals) · T0.M0.4 (two-source cost model) |
| Time | 9 minutes |
1. Concept
A budget’s spend so far is worked out fresh every single time it is asked for.
There is no stored total anywhere in the database. Every dashboard, every API call and every notification adds it up again from the underlying cost records, within that budget’s scope and dates.
That sounds wasteful and is deliberate. A stored total can be wrong; a total computed from the records cannot disagree with them.
ARCHITECTURE: LIVE COMPUTATION:
Read a budget ↓ SELECT SUM(costColumn(hasBilling)) FROM cost_records WHERE (scope matches budget's filter) AND (date in current budget period) ↓ Return the sum
ARCHITECTURE NOT USED: STORED CURRENT_SPEND: Cron writes current_spend_usd column periodically Read budget → return stored value (the original design; the column was dropped in April 2026)The live-compute architecture is what makes budgets always-current and always-correct. It is also what lets a change to a budget’s amount or threshold take effect on the very next read.
Why live computation
Four reasons, in order of importance:
1. ACCURACY Always current. No stale data. New cost records show up in the budget reading immediately (next read). Stored values lag by however long the last cron ran.
2. SIMPLICITY One source of truth (cost_records). No sync logic. Stored values require maintenance: - When cost records change, recompute - When a group's membership changes, recompute - Handle race conditions during recompute - Detect and reconcile drift None of this exists in the live-compute model.
3. FLEXIBILITY Every target type (resource, group, account, AI provider) is just a different filter on the same query. Stored would need a maintained total per target, kept in step with every membership change.
4. CORRECTNESS No drift. The computation matches what reports show. Stored values can diverge from actuals (rare, but possible bugs cause this; we eliminate the class of bug entirely).Performance characteristics
The concern with live computation is performance. Aren’t aggregate SQL queries slow?
TYPICAL BUDGET READ COSTS (with proper indexing):
Per-resource budget: ~5 ms (single resource_uid filter) Per-group budget: ~10-25 ms (small to medium group) Per-account budget: ~10-50 ms (one account filter) Org-wide rollup: ~50-150 ms (full table scan with date filter)
BUDGET HEALTH WIDGET listing 12 budgets: ~200-400 ms total Fast enough for sub-second page renders.The indexing strategy is the key. The cost_records table is indexed on (org_id, date, scope_columns): the aggregate queries hit the index, not the full table.
Compare to the stored alternative:
STORED-COLUMN READ COSTS: Per budget read: 1-2 ms (single row read)
But: more complex sync logic Stored values lag behind actuals by minutes-to-hours Re-computation on membership change adds latency to writeThe 10-50 ms live-compute cost is the right trade for the simplicity gains. Sub-100ms is well within UI responsiveness budgets.
The costColumn() function
The SQL aggregation uses a helper, costColumn(), that picks the cost source per the two-source model (T0.M0.4):
costColumn(): always → COALESCE(actual_cost_usd, cost_usd)It once took a hasBilling switch; since June 2026 it ignores it and always prefers the billed figure. In practice:
- Where billing sync has filled in actual (post-discount, billing-data) cost for a day, that is used
- Where it has not (no billing sync, or days not yet synced), the calculated rack-rate cost is used
The result: budgets reflect what the customer will actually pay (when billing data is available) and what we think they will pay (when it isn’t), automatically.
What live compute means for budgets
1. SPEND UPDATES IMMEDIATELY when new cost records land - Daily: the cost-and-rollup job writes new rack-rate cost records (19:55 UTC) - Daily: billing sync updates actual_cost_usd (20:20 UTC) - In both cases, next budget read picks up the new value - Budget transitions from green to yellow to red as data arrives; no batch processing delay
2. ONE NUMBER, NO COST-SOURCE LABEL - The Budgets page shows a single spend figure per budget - It does not say which days are billed and which are rack rate - If billing sync has not run for recent days, those days are rack rate (T0.M0.4); check the sync before arguing about cents
3. BUDGET CHANGES TAKE EFFECT IMMEDIATELY - Threshold changed: next read evaluates status against the new threshold - Amount changed: next read re-evaluates % used and status - No stored spend to "recompute": there is nothing to migrate - The target is fixed: to watch a different resource, group or account, create a new budget on it (one target per budget); its spend is there on the first readNotification firing
Alerts fire from the daily cost-and-rollup job (19:55 UTC), using the same live-computation logic:
DAILY COST-AND-ROLLUP JOB:
For each active budget: spend = live_compute_spend(budget) level = exceeded if spend >= 100% of the budget warning if spend >= the budget's alert threshold none otherwise
If a channel subscribes to budget.warning / budget.exceeded: notify that channel (again on each daily run while the level holds) Else (no channel configured): email once per level per month, recorded in budget_alert_state.alerted_level; re-armed if spend falls back below the levelThe single stored field is that email bookkeeping flag, not the spend value. The spend itself is computed live. Alerts are not written to the audit log, which records API changes only.
Historical migration
ZopNight originally stored current_spend_usd in the cost service’s own budgets table. That table was dropped in April 2026.
MIGRATION (2026-04-30): 1. Move budget definitions to the configuration service and convert all budget reads to live computation Validate against the stored value (parity check) No customer-visible change
2. Drop the old budgets table, and its stored column The daily job stays: it still computes spend live to fire alerts, but no longer writes spend back
CUSTOMER IMPACT: zero Reads use the same underlying cost data, just computed live. Performance is comparable (within 50 ms). Accuracy improved (no stale-data window). Audit log is unchanged.The architecture is intentionally lean. Storing a value that can be computed from other values is duplication; duplication invites drift; drift causes audit headaches. Live compute eliminates the class.
When live compute would NOT be the right choice
A useful counterfactual:
LIVE COMPUTE BREAKS DOWN if: - Underlying data is very expensive to query (billion-row scans without indexing) - Aggregation is recursive or graph-walking - Realtime requirement is sub-millisecond (it's not for budgets)
For ZopNight's cost_records table: - Indexed appropriately; queries hit indices - Aggregation is straightforward SUM with date + scope filter - 50 ms is fine for budgets
So live compute is the right answer here.For a different domain (e.g., a real-time game scoreboard), the trade-offs might shift. For budgets specifically, live compute wins.
How ZopNight uses live computation
Every surface that displays budget spend reads it live: dashboard widgets, the Budgets page, the API (GET /orgs/{orgID}/budgets), and the daily alert job. All of them share the same live-spend computation: there is no second source-of-truth that could disagree.
For customers who want to export budget state, the API returns the computed value at the moment of the call. Snapshotting a budget’s spend at a specific time means calling the API at that time; the system does not retain historical “spend at point X” values (those can be derived from cost_records time-series if needed).
2. Demo
A threshold-and-amount change scenario:
SCENARIO: Customer tunes a budget mid-month.
T+0 Budgets page, dev-platform-eu group budget Spend: $4,200 of $4,800 (87.5%, live computed) Alert threshold: 90% → status green
T+30 s Edit the budget; set the alert threshold to 80% Save
T+31 s Budgets page refreshes Same spend, $4,200 (87.5%) 87.5% is now above 80% → status yellow
T+60 s Edit again; raise the amount to $6,000 Save
T+61 s Budgets page refreshes $4,200 of $6,000 = 70% → status green
T+2 min The team also wants staging-eu watched A budget's target cannot be edited, so create a new budget on the staging-eu group: $2,600/month First read: spend $1,950 (75%), computed on the spot
NO BATCH delay. NO cron wait. NO job to rerun.
This works because: - The budget definition stores the target, amount and threshold - Every read sums cost_records for that target - Status is derived on the read from spend, amount and threshold - No stored value to refresh
CONTRAST with stored-column architecture: - Save would trigger a recompute job - User refreshes; sees stale value until job completes - User wonders if the change took effect - Possible race condition between recompute and readThe user gets immediate feedback. The system gets simpler. Both win.
3. Hands-on (5 min)
Verify live computation:
1. Open the Budgets page in your org; pick one budget Current spend (note value): $__________ % used and status: __________
2. Come back after the next daily cost run (after 20:20 UTC) Refresh the Budgets page Has the spend value changed? Yes / No New value: $__________
3. Edit the budget's alert threshold to just below its current % used. Did the status turn yellow on the next read? Yes / No Set the threshold back afterwards.
4. Audit observation: Did the budget transition green/yellow/red during the month? __________
5. Architectural check: Call the API: GET /orgs/{orgID}/budgets Your budget's entry should show the same computed spend API response: $__________ (matches UI? yes / no)If the API and UI values diverge, that’s a bug worth reporting: they should always match because they share the live-compute function.
4. Knowledge check
Q1
A budget’s current spend is:
A. Computed live on every read by aggregating cost_records for the budget’s scope and current period
B. Stored in a database column for fast reads and then refreshed on a nightly cron schedule
C. Read straight from the cloud provider’s own budget API on each and every request made
D. Cached for one hour after it has first been computed for that period, and then reused
Show answer
Correct: A. Always current; no stale data; no sync logic. The costColumn() helper prefers billed cost and falls back to rack rate per the two-source model. Live computation is the architectural choice. The current_spend_usd column was dropped in April 2026; the design is intentional.
Q2
A new cost record lands at 14:25 UTC. The budget reflects:
A. After the next cron run (could be hours)
B. After 24 hours
C. Immediately, on the next budget read
D. Only once the period rolls over to the next month
Show answer
Correct: C. Live computation means latency = SQL query time (~10-50 ms). The budget transitions across thresholds in near-real-time as cost records arrive. Immediate. There is no batch step between cost arrival and budget visibility.
Q3
Why no stored current_spend column?
A. Simplicity and correctness
B. Performance: stored would be slower
C. It would break the polymorphic budget scoping
D. Database constraints
Show answer
Correct: A. One source of truth (cost_records) Stored would require sync logic, risk drift, and add complexity. Live compute is fast enough (10-50 ms typical) for the UI responsiveness budget. The trade-off favors live compute decisively. Architectural decision for simplicity. Performance is comparable; correctness is improved.
5. Apply
The Budgets page and the Budget Health dashboard widget show live-computed spend. No additional configuration; the architecture handles it.
For customers debugging “why is my budget showing X” questions, the answer is always: look at the underlying cost_records for the budget’s scope and time window. The budget number is a SUM; the components are queryable.
Related lessons
- L1: Budget vs forecast vs alert
- L2: Budget scopes
- L3: Threshold-crossing notifications
- L4: Green/yellow/red signals
- T0.M0.4.L4: The two-source cost model
Module quiz
Complete M4.6 → 10-question module quiz.