Note
Published
2026-07-28
Reading
15 min
Data
Public sources
Headline figure
High
Claim
What FOCUS normalizes, and the five things it does not
FOCUS gives three clouds one column vocabulary and one definition of a cost basis, which removes most of the tedium from multi-cloud reporting. It does not normalize the ownership dimension, the timing of credits, where an unused commitment charge lands, or the fact that no export is retroactive, so the native export stays the source of record per cloud.
Topics: method · AWS · Azure · GCP
Built from public provider documentation and sourced research. Worked figures inside are labelled synthetic. No client data was used.
Argument
The claim
If you run three clouds and want one number, FOCUS is the right normalization layer and you should use it. All the major providers now export FOCUS-conformant billing data natively, and version 1.4, published in June 2026, added Invoice Detail and Billing Period datasets that make true invoice reconciliation possible against the spec rather than against each vendor’s own schema.1
That is the good news and it is genuinely large. What follows is the part that tends to be skipped in multi-cloud pitches: five differences FOCUS does not and structurally cannot remove. Each of them requires per-cloud code, and each of them has broken somebody’s cost-per-customer model.
First, the export of record per cloud
Before any normalization, you need to be reading the right file. There is exactly one correct answer per cloud, and the wrong answers all look plausible.
| Cloud | Export of record | Destination | The plausible wrong answer |
|---|---|---|---|
| AWS | CUR 2.0, Parquet | S3, queried via Athena with partition projection | Cost Explorer. Hourly and resource-level detail is retained 14 days, so it cannot answer a 90-day question |
| Azure | Cost Management exports, both amortized and actual | ADLS Gen2 container | Either dataset alone. Only amortized attributes commitment benefit to the consuming resource; only actual shows the cash |
| GCP | Detailed usage cost export, gcp_billing_export_resource_v1_<ACCOUNT_ID>, plus the pricing export |
BigQuery | The standard usage export. It has no resource-level names, so it cannot support resource attribution |
Two of those three are two-file answers. That is not pedantry: modelling Azure on the amortized dataset alone produces a total that does not match the invoice, and modelling on actual alone produces per-resource costs that ignore every reservation you bought. Both are wrong in opposite directions, which is why the error survives review: the numbers look reasonable individually.
What FOCUS actually gives you
The valuable part of the specification is that it settles the cost-basis argument. Four columns, one meaning each, across every provider:
| FOCUS column | Means | AWS CUR 2.0 | Azure | GCP |
|---|---|---|---|---|
BilledCost |
What appears on the invoice for the period | line_item_unblended_cost |
actual dataset cost | cost plus credits as invoiced |
EffectiveCost |
Amortized cost including commitment benefit | savings_plan_savings_plan_effective_cost, reservation_effective_cost |
amortized dataset cost | cost plus credits filtered by credits.type |
ListCost |
Public rate, no discount of any kind | pricing_public_on_demand_cost |
PayGPrice × quantity |
list price from the pricing export |
ContractedCost |
Negotiated rate before commitment benefit | line_item_net_unblended_cost |
UnitPrice or EffectivePrice, per agreement type |
effective_price from the pricing export |
This mapping is the single highest-value thing in the specification, because picking the wrong basis is the most consequential error available in this work and it is completely silent. Build a cost-per-customer model on unblended cost and every unit-economics number you produce is wrong by exactly your commitment discount: uniformly, plausibly, and in the direction that makes margin look worse than it is. Build it on list and you are reporting a price you do not pay.
For unit economics the answer is EffectiveCost, every time. For “does this
match the invoice” the answer is BilledCost. Reporting one and labelling it
the other is how a model loses an argument in a readout.
Which column carries amortized cost on each provider
If you are reading a native export rather than a FOCUS one (which you should be, per the section above) then “amortized effective cost” is a different piece of work on each cloud, and it is worth writing out because the phrase makes it sound like a column that exists.
- AWS: no single column. Amortized effective cost is assembled by
line_item_line_item_type:savings_plan_savings_plan_effective_costfor Savings Plan covered usage,reservation_effective_costfor discounted usage, andline_item_unblended_costfor everything else. Sum the wrong combination and you double-count the commitment. - Azure: a different dataset, not a different column. The amortized dataset attributes commitment benefit to the consuming resource; the actual dataset shows the cash that left the account. You export both, model on amortized, and reconcile cash against actual.
- GCP:
costplus credits, with the credits filtered bycredits.type. Which types you include is the definition of amortized on GCP, and it is a decision you have to write down: committed and sustained use discounts belong in it, promotional credits do not. - FOCUS-native exports: one column,
EffectiveCost, on all three. This is the specification earning its keep, and it is why the FOCUS export is worth running as a cross-check even where it is not the pipeline.
The five things FOCUS does not normalize
1. The ownership dimension
FOCUS standardizes cost. It does not standardize who owes it. Tags and labels arrive as provider-extension columns, so the allocation seam stays provider-specific by design:
- AWS:
resource_tags_user_*columns exist only for tags activated in the Billing console, and activation is not retroactive. - Azure: tags do not flow from a resource group down to resource usage rows
unless the Cost Management tag inheritance setting is on or an Azure Policy
modify rule does it. Several meters never carry tags at all: parts of
Microsoft.Network, bandwidth, and some platform meters. - GCP: labels land in the export, and the resource-level export is the only one that carries resource names.
The consequence is that a FOCUS-normalized warehouse still needs three tagging strategies, three enforcement paths, and one reconciliation of the three against a single org chart. That last part is the actual work, and it is organisational rather than technical.
2. Retroactivity, the one that cannot be fixed later
None of the three exports backfill. AWS cost allocation tags populate CUR columns from the day they are activated forward. GCP’s resource-level BigQuery export starts writing on enable. Azure tag inheritance applies going forward. Same for the per-service switches: EKS split cost allocation data, GKE cost allocation, the AKS Cost Analysis add-on, Bedrock model invocation logging, Azure OpenAI diagnostic settings.
This is the only item on this list where delay has a price. Every week without these enabled is a week of history that no future project can recover, which is why “turn the exports on” is worth doing before anyone has decided what to measure.
The failure it produces is predictable enough to write down in advance. A team commits to a cost-per-customer number for a board meeting, starts the work, and discovers in week two that the ninety days it needs to reason about are ninety days of untagged rows. There is no query that recovers them, there is no support ticket that recovers them, and the only variable anybody controls is how many further days are lost before the switches are flipped. That is the entire reason the cheapest thing on this site is a three-day export check rather than an analysis.
3. Credit and discount mechanics
FOCUS defines what an amortized cost is. It does not make the three providers arrive at one the same way.
- GCP delivers credits as separate negative-amount rows that must be
filtered by
credits.typeto separate sustained use discounts, committed use discount credits, promotional credits and free tier. Summingcostalone overstates spend; summing every credit blindly double-discounts it. Both errors are common and they point in opposite directions. - Spend-based CUD credits float across projects within a billing account, so a project can show a discount it did not commit to. That is an allocation policy decision, and it has to be made explicitly and disclosed.
- Azure behaves differently by agreement type:
EffectivePrice,UnitPriceandPayGPricedo not mean the same thing under EA and MCA, and the hierarchy differs too: MCA exposes billing profiles and invoice sections, EA exposes departments and enrollment accounts.
4. Where an unused commitment charge lands
This one is worth its own heading because it hides waste rather than mis-stating it.
On Azure, unused reservation charges land on the billing profile or enrollment, not on the subscription that should have consumed the capacity. A tidy subscription-level showback report will therefore show every team inside tolerance while the reservation waste sits above them all, attributed to nobody. The fix is not a query change; it is reconciling at the billing scope and allocating the unused charge down under a stated rule.
GCP has the mirror-image version: resource-based CUDs are locked to a region and machine family, and a machine-family migration strands them. Spend-based CUDs are denominated in dollars per hour, and flexible CUDs are a third shape again. FOCUS reports all of this correctly as cost. It has no view on whether the commitment was a mistake.
5. GCP’s own FOCUS export is still Preview
Checked August 2026: Google’s FOCUS export in BigQuery is a Preview feature. MEDIUM CONFIDENCE: availability status verified 2026-08-10; provider preview status changes without notice, so re-check before relying on it
The FOCUS project lists Google Cloud among the providers exporting natively, which is accurate.1 Both things are true at once, and the practical reading is unglamorous: use GCP’s FOCUS export as a cross-check against the detailed usage export, never as the sole pipeline. A Preview surface can change shape, and a reconciliation harness that silently depends on one is a harness that will fail during someone’s quarter close.
The inventory to run before you model anything
None of the above is exotic, and none of it requires a vendor. It is the first hour of any competent teardown and it is entirely checkable from inside your own console under read-only credentials. The list, in the order we run it:
| # | Question | What a bad answer means |
|---|---|---|
| 1 | Is CUR 2.0 exporting to S3 as Parquet, and since when? | Your history starts at that date, not at your incorporation |
| 2 | Which cost allocation tag keys are activated in the Billing console? | Unactivated keys have no resource_tags_user_* column at all |
| 3 | Are both Azure exports running, amortized and actual? | One dataset gives you either the cash or the attribution, never both |
| 4 | Is Azure tag inheritance on, or an Azure Policy modify rule doing it? | Resource-group tags never reach usage rows; allocation coverage collapses |
| 5 | Is the GCP detailed usage export enabled, plus the pricing export? | The standard export carries no resource names, so no resource attribution |
| 6 | Is EKS split cost allocation data on? GKE cost allocation? AKS Cost Analysis? | Cluster cost cannot be allocated below the node for the period before it was enabled |
| 7 | Is Bedrock model invocation logging on? Azure OpenAI diagnostic settings? | No token records exist for the window you want to analyse |
| 8 | What share of cost carries a usable owner dimension, measured per cloud? | Below roughly 70%, the allocation is a rule rather than a measurement |
| 9 | Which meters emit no tags at all on your estate? | NAT gateway data processing, cross-AZ transfer, Log Analytics ingestion, parts of Microsoft.Network. These need a split rule, not a tag policy |
| 10 | Does each export's own total reconcile to its invoice today? | If it does not, nothing downstream of it can be trusted |
Question 8 is the one that decides whether a unit-economics project is ready. Measure it, do not estimate it: sum cost where the owner dimension is non-null, divide by total cost, per cloud, for a full billing month. Question 10 is the subject of the next section, and of a cost model that ties out exactly is hiding something.
The reconciliation, which is the only proof that matters
Whatever the pipeline, the same test decides whether to believe it: does the sum of the modelled rows equal the invoice, per cloud, per billing period, within a stated tolerance? Mine is 3%.
-- AWS: does the modelled effective cost reconcile to the invoice?
select
bill_billing_period_start_date as period,
bill_payer_account_id as payer,
sum(case
when line_item_line_item_type = 'SavingsPlanCoveredUsage'
then savings_plan_savings_plan_effective_cost
when line_item_line_item_type = 'DiscountedUsage'
then reservation_effective_cost
when line_item_line_item_type in ('Usage','Fee','Tax','Refund','Credit')
then line_item_unblended_cost
else 0
end) as modelled_effective_cost,
sum(line_item_unblended_cost) as billed_cost
from cur2
where bill_billing_period_start_date = date '2026-07-01'
group by 1, 2;
-- GCP: amortized cost is cost plus credits, and credits must be typed
select
invoice.month as period,
project.id as project,
sum(cost) as gross_cost,
sum((select sum(c.amount) from unnest(credits) c
where c.type in ('COMMITTED_USAGE_DISCOUNT',
'SUSTAINED_USAGE_DISCOUNT'))) as commitment_credits,
sum((select sum(c.amount) from unnest(credits) c
where c.type = 'PROMOTION')) as promotional_credits,
sum(cost) + sum((select sum(c.amount) from unnest(credits) c))
as net_cost
from `billing.gcp_billing_export_resource_v1_XXXXXX`
where invoice.month = '202607'
group by 1, 2;
Note that the GCP query separates promotional credits from commitment credits rather than netting them together. Promotional credits are not a discount you earned and they will run out; a unit-cost model that includes them forecasts a margin you do not have. Azure gets the equivalent treatment against the amortized dataset with the actual dataset as the cash check.
The residual is the deliverable
Reconciliation never lands on zero, and the honest output is a named residual rather than a clean number. The sources we report by name: provider token and unit rounding, sub-hourly boundary effects, credit application timing, MCA currency conversion, and usage-API reporting lag where a provider’s API rather than an export is involved.
The rule that matters more than the tolerance: report the residual, do not distribute it. Spreading an unexplained 2.4% across teams to make the table sum correctly destroys the one signal that tells a reader whether the rest of the model is sound.
The change no single-cloud detector can see
One consequence of all this is worth stating on its own, because it is the strongest practical argument for normalizing at all. It is not a reporting argument, it is a detection one.
Native anomaly detection is per provider. So consider the changes that actually move a multi-cloud bill: a workload relocated from GCP to AWS, a model swapped from Bedrock to Azure OpenAI, a batch job moved off a self-hosted GPU pool onto a per-token API. Each provider sees either a decrease, which nothing alerts on, or a plausible increase inside tolerance. The total can move materially with no detector firing anywhere, because no detector is looking at the total.
The same blindness applies to commitment posture, and there the fix is to read coverage and utilization as two separate diagnoses rather than one blended percentage. High utilization with low coverage means you are under-committed and paying on-demand for steady baseline. Low utilization with high coverage means you bought capacity you are not consuming. They are opposite problems with opposite fixes, and a single blended number hides both, which is how a reservation expires without a decision having been made about it.
Neither of these is exotic analysis. Both require one thing: all three bills read against one definition of a dollar, on a schedule. That is what the normalization is for, and it is a better reason to do it than tidiness.
What would change our mind
If FOCUS adoption reached the point where a provider’s native schema was the translation layer and FOCUS was the source of record, with tags normalized into first-class columns and commitment-waste placement standardized, then multi-cloud normalization becomes a configuration exercise and this note becomes history. The 1.4 Invoice Detail and Billing Period datasets are a real step toward it.
Until then the working rule is the unromantic one: FOCUS is the shared vocabulary you report in, and the native export per cloud is the thing you reconcile against.
The same hour, priced four ways
Relocated from /methodology in the W2 subtraction pass. 17,520 instance-hours, 80% covered by a three-year all-upfront Savings Plan at 0.2560 USD/hr against 0.4032 on demand. The upfront cash landed in a prior period.
| Basis | FOCUS 1.1 column | CUR field | Amount USD | vs amortized |
|---|---|---|---|---|
| List | ListCost | pricing_public_on_demand_cost | 7,064.06 | +41.3% |
| Billed, this period | BilledCost | line_item_unblended_cost + fee lines | 1,412.81 | −71.7% |
| Unblended | no equivalent | line_item_unblended_cost | 1,412.81 | −71.7% |
| Amortized effective | EffectiveCost | savings_plan_savings_plan_effective_cost | 5,000.91 | 0.0% |
| Spread, list to billed | 5,651.25 | 113.0% |
Sources
- FOCUS specification, native provider adoption, and the 1.4 Invoice Detail and Billing Period datasets (June 2026): focus.finops.org, amnic.com. High confidence.
- AWS billing access control, for the read-only role these queries run under: docs.aws.amazon.com. High confidence.
Sources are listed at the foot of this note with their confidence stated. Medium and low confidence figures say so in the copy.
Every query here runs against a read-only role you create, scope and revoke. Access policy.
Close
More notes
The rung this argument belongs to
Instrumentation Build · Fixed fee per stage
Check the arithmetic yourself
The full derivations, with the queries written out so they run under your own read-only credentials, in your own console.