Every month the bill is diffed against the budget you set, every movement is attributed to the team, service or decision behind it, and the result is written up for a finance partner to read without an engineer beside them.
| Rank | Movement | Line | Attributed to |
|---|---|---|---|
| 1 | +3 120 | NAT gateway, eu-west-1 | cross-zone traffic since the 04 Sep deploy of svc-media |
| 2 | +1 940 | EC2 on demand, m6i.2xlarge | reserved capacity expired 31 Aug, not renewed |
| 3 | +870 | RDS storage | autoscaling storage on analytics-replica, no ceiling set |
| 4 | -610 | S3 standard | lifecycle rule moved 40 TB to infrequent access |
Readings from the last run.
01The problem
Cost Explorer, the billing exports and the FinOps dashboards all answer the same question: how much, by which service, over which period. Finance asks a different one: why did it move, who owns the movement, and is it inside the number agreed in January. That answer is assembled by hand, by an engineer, once a quarter, in a spreadsheet nobody trusts a month later.
Budgets are set once a year and checked when the invoice hurts. By then the NAT gateway has been routing cross-zone traffic for three months, the environment nobody deleted has been running since the demo, and the reserved capacity that expired in March has been billed on demand since April. Each is visible in the export. None of it is visible as a sentence.
cost-diff produces the ranked diff of two periods. Around it sit the schedule, the budget to compare against, the attribution to a team or a decision, and the writing. That is the hosted layer.
02The deliverable
A written account of the month with the figures inside the sentences, addressed to the reader who signs off the number rather than to the one who runs the console.
Cloud spend in September was 53 660, 11.8% above the monthly budget of 48 000. Year to date the account has spent 447 900 against 432 000 budgeted, so the annual number is still recoverable and this month is the one that decides it.
Most of the movement is a single line. The NAT gateway in eu-west-1 added 3 120 after the deploy of svc-media on 04 September began routing traffic across zones. A VPC endpoint for the two buckets it reads removes the charge without touching the service.
Reserved capacity on the m6i.2xlarge fleet expired on 31 August and was not renewed, so 1 940 of steady-state compute was billed on demand. Renewing on the same term returns the line to budget from the first of next month and changes nothing that is running.
Storage on analytics-replica autoscales with no ceiling set and added 870; a ceiling at its current size holds the line. Against that, the lifecycle rule applied in August moved 40 TB to infrequent access and took 610 off S3, the first month that saving is visible in the bill.
Unowned spend is 7 510, 14% of the bill, spread across 61 resources carrying no team tag. That share of the bill cannot be attributed to anyone, and it will appear in every report until the tags exist.
Left alone, October forecasts at 56 900 and breaches the budget in week two. The three actions named above recover 5 930 between them and none of them needs a code change.
Prepared for acme-prod · period closed 30 September 2026
03What it does
Budget season is one hour of it. The rest is the twelve reports that hold the number afterwards.
Twelve months of billing history is read, per-service growth is separated from seasonality, and the line items come out in the shape finance asks for. The spreadsheet that used to take a fortnight.
Each closed period is compared with the budget line and with the period before it: the variance, the cause of it, and where the year lands if nothing changes. Forwarded as it arrives, with nothing to translate first.
A service trending past its budget line is flagged while the period is still open, by email, Slack or webhook, rather than confirmed by the invoice four weeks later.
Three more GPU instances in the second quarter, or Postgres moved off RDS: the change is entered against the current run rate and the revised forecast and budget impact come back with it.
Tags, account structure and Kubernetes labels allocate the bill, so each team reads its own budget and its own actuals without learning a FinOps vocabulary. What none of them accounts for is reported as unowned.
Not a count of idle resources: what to change, what it recovers, and whether it takes a deploy or a setting, ranked by the amount recovered and carried forward until it is done or dismissed.
04How it works
A read-only role, no agent to install, and nothing but cost data read from the account. Four steps: connect, compare, attribute, write.
Billing exports from AWS, GCP or Azure, read-only, plus the tags and the Kubernetes labels that carry ownership. A budget per account or team is declared once.
Every period is diffed against the previous one and against the budget line, with cost-diff's ranking of the movers.
Each movement is tied to a team, a service or a dated change where the data allows. Where it does not, the spend is reported as unowned, which is itself the finding.
A monthly report in plain language: what moved, why, who owns it, what the forecast says, and which of idle-hunter's and k8s-rightsizer-report's suggestions would close the gap. Sent to the people who asked for it, with the numbers attached for anyone who wants to check.
05Who it is for
A cloud number was agreed with finance in January. The first sign it is off arrives with the invoice, three months of drift later, with an afternoon of spreadsheet work to explain it.
Reads the report monthly, forwards it, and never has to open a console or ask what a NAT gateway is to know whether the number holds.
06What it costs
One price per billing account, with the report and every reader included.
Charging per seat would keep the report away from exactly the people it is written for.
07Request access
Requests decide which provider's export is read first.
08Objections