FinOps FOCUS Specification: Normalizing Cloud Billing Data Across AWS, Azure and GCP (2026)

FinOps FOCUS Specification: Normalizing Cloud Billing Data Across AWS, Azure and GCP (2026)

FinOps FOCUS Specification: Normalizing Cloud Billing Data Across AWS, Azure and GCP

Ask three clouds what you spent last month and you get three different vocabularies. AWS says “line item”, Azure says “meter”, Google says “SKU”, and each one splits discounts, credits and commitment purchases across different columns with different sign conventions. Every FinOps team has written the same brittle mapping layer: a few hundred lines of SQL that nobody trusts at month end, because the invoice never quite reconciles to the dashboard.

The FinOps FOCUS specification (FinOps Open Cost and Usage Specification) is the industry’s attempt to retire that mapping layer. It defines one column model, one set of enumerations and one set of rules for how discounts, amortization and shared cost are expressed, so that provider exports arrive already shaped alike. It is open, vendor-neutral, and now published by the FinOps Foundation, a Linux Foundation project, with native exports from the major clouds.

But “native FOCUS export” does not mean “no pipeline”. Versions differ by provider, optional columns are null more often than you expect, and allocation is still your job. This article is the practitioner’s walk-through.

What this covers: the FOCUS column model and the four cost columns, how AWS, Azure and GCP exports map to it, commitment amortization with worked numbers, a pipeline architecture from object store to dashboards, a runnable DuckDB SQL and Python snippet, shared-cost allocation, unit economics, and the failure modes that bite.

This is a systems and data-engineering analysis, not financial advice.

Context and Background

Cloud bills are produced by pricing engines built independently over fifteen years. AWS’s Cost and Usage Report (CUR) is a wide table with hundreds of columns whose populated subset depends on the service and the discount type. Azure Cost Management exports differ between Enterprise Agreement and Microsoft Customer Agreement accounts, and Microsoft’s own documentation describes column renames across schema versions. Google Cloud writes billing export to BigQuery with nested, repeated fields for credits, labels and system labels. None of these was designed to be joined to the others.

FinOps practitioners compensated with warehouse transformations. That works until a provider adds a discount program, changes how a commitment is represented, or backfills a month. Each change silently breaks a downstream model, and multi-cloud showback becomes a standing engineering tax rather than a reporting task. The FinOps Foundation’s FOCUS working group began in 2023 to replace this with a shared schema, and the specification is licensed openly (CC BY 4.0) so tool vendors can implement it without negotiation.

The version history matters for planning, so here is what I could verify this month. The specification site lists versions 0.5, 1.0, 1.1, 1.2, 1.3 and 1.4, with 1.4 labelled as the current publication on the FOCUS specification page. The Linux Foundation announced FOCUS 1.3 on December 11, 2025, adding allocation columns for shared costs, a separate contract commitments dataset, data recency and completeness metadata, and service and host provider columns. The 1.4 changelog on the specification site reports 47 added columns, 2 removed (Provider and Publisher, both deprecated) and 14 changed, with new billing period, invoice detail and contract commitment fields. A secondary article puts the 1.4 ratification at June 4, 2026; I could not confirm that date on the primary site, so treat it as reported.

Provider support lags the spec, which is the single most important operational fact. AWS announced general availability of Data Exports for FOCUS 1.2 on November 19, 2025. Microsoft’s schema documentation lists a 1.2-preview with 105 columns. Google Cloud’s published material describes a BigQuery view that transforms standard billing export into FOCUS format, documented at 1.0 GA with a Looker template. Secondary reporting from October 2026 places all three clouds at about the 1.2 level or below and none at 1.4. Treat that as unverified until you open the manifest of your own export. The lesson: your pipeline must be version aware, not hard-wired to one release.

If you already run Kubernetes cost tooling, FOCUS complements it rather than replacing it. Our guide to OpenCost Kubernetes cost allocation, VPA rightsizing, showback and chargeback covers the in-cluster half of the story. FOCUS covers the provider invoice half, and the 1.3 allocation columns are designed so that the two meet in the same table.

The FOCUS Column Model: Four Costs, One Row Per Charge

Direct answer: FOCUS represents every line of a cloud bill as one row with a fixed set of columns. The four cost columns, ListCost, ContractedCost, BilledCost and EffectiveCost, express the same charge at progressively more realistic prices: public list, negotiated, invoiced, and amortized. Choosing the right one for the question you are answering is the core skill.

FinOps FOCUS specification cost columns from ListCost through ContractedCost and BilledCost to EffectiveCost, with commitment purchase and covered usage feeding the amortized view

Figure 1: The four FOCUS cost columns. List and contracted prices explain discount depth; billed cost matches the invoice; effective cost spreads commitment purchases over the usage they cover.

Figure 1 shows the relationship. ListCost is the list unit price multiplied by the pricing quantity, which tells you what the usage would cost with no negotiated deal. ContractedCost uses the unit price you actually negotiated under your agreement, before commitment programs. BilledCost is the amount that lands on the invoice for that row, which is why it is the column to reconcile against the invoice. EffectiveCost is the amortized cost after reduced rates, discounts and the applicable share of prepaid purchases.

Why four columns instead of one

Every other billing format forces you to pick one notion of cost and reconstruct the rest. FOCUS keeps all four in the row so that the same data answers different audiences. Finance wants BilledCost because it ties to cash and the invoice. Engineering wants EffectiveCost because it attributes the commitment discount to the workloads that consumed it. Procurement wants the gap between ListCost and ContractedCost to measure how well the enterprise agreement performs.

According to the specification’s column page, EffectiveCost is a mandatory decimal metric that cannot be null, expressed in the billing currency. It is the amortized cost of a charge after applying all reduced rates, discounts and the applicable portion of relevant prepaid purchases. The amortized portion should be proportional to the pricing quantity and the time granularity of the data. Once a purchase has been amortized to later eligible charges, the original purchase row’s EffectiveCost is set to zero.

That last rule has a consequence people miss. For a given commitment discount, the total EffectiveCost of Usage charges must equal the total BilledCost of Purchase charges. That identity is your first free data-quality test, and we will turn it into a dbt test later. Another caveat from the specification: the billing period total of EffectiveCost may differ from the invoice totals for the same billing account, so never promise stakeholders that amortized spend equals the invoice.

Charge categories and classes

Alongside the costs, a handful of enumerated columns decide how a row should be interpreted. ChargeCategory classifies the nature of how the charge is billed; in the 1.x line the values I know are Usage, Purchase, Tax, Credit and Adjustment, though the column page I fetched states only that the value must be one of the allowed values, so confirm the list against your target version. ChargeClass flags corrections to a previous billing period, which matters because late corrections otherwise look like a spend spike in the wrong month. ChargeFrequency distinguishes one-time, recurring and usage-based charges.

PricingCategory describes how the price was determined: standard on-demand, dynamic such as spot, committed, or other. Two of these four are worth defending in code review. Never sum EffectiveCost across ChargeCategory values without deciding whether Tax and Credit belong in your KPI. And never compare unit prices across rows with different PricingCategory values without segmenting, because a spot hour and a committed hour are different products.

Commitment discount columns

The commitment columns identify what discount program covered a row. Microsoft’s documentation lists CommitmentDiscountCategory, CommitmentDiscountId, CommitmentDiscountName, CommitmentDiscountQuantity, CommitmentDiscountStatus, CommitmentDiscountType and CommitmentDiscountUnit. In the specification the category distinguishes spend-based programs (a dollar-per-hour commitment, like AWS Savings Plans) from usage-based programs (a quantity commitment, like a reserved instance). The status column indicates whether the committed amount was used or went unused, which is how you surface waste: unused commitment is a cost nobody’s workload is carrying.

SKU and pricing columns round out the model. SkuId and SkuPriceId identify the product and its price, ListUnitPrice and ContractedUnitPrice give the rates, and PricingQuantity and PricingUnit describe what you were charged for, while ConsumedQuantity and ConsumedUnit describe what was actually used. The distinction between pricing and consumed quantity trips teams up: you may be billed per started hour while consuming 40 minutes, and unit economics should usually use consumed quantity.

Required, conditional, and x_ columns

Not every column is populated on every row. FOCUS marks columns as mandatory, conditional or recommended, and conditional columns must be null when they do not apply. That is a feature, not a bug: a null CommitmentDiscountId means “no commitment covered this row”, and your pipeline should preserve that meaning rather than backfilling.

Providers extend the schema with custom columns prefixed x_. AWS’s FOCUS 1.2 table adds three, x_Discounts, x_Operation and x_ServiceCode. Azure adds many, including x_AmortizationClass, x_BillingProfileId, x_ResourceGroupName and x_ResourceType. These are where provider-specific truth lives, such as Azure resource group, which has no FOCUS equivalent. Keep them, but isolate them in a side table keyed by row so that your core model stays portable.

Mapping AWS, Azure and GCP Exports to FOCUS

Direct answer: AWS and Azure now offer native FOCUS-shaped exports (AWS Data Exports “FOCUS 1.2 with AWS columns”, Azure Cost Management’s FOCUS dataset), while Google Cloud provides a BigQuery view that transforms its standard billing export into FOCUS. All three emit the same core column names, but at different spec versions and with provider-specific x_ extensions.

FinOps FOCUS specification pipeline stage mapping AWS, Azure and GCP billing exports into a unified table through a version-aware adapter

Figure 2: A version-aware adapter layer. Each provider lands in its own raw zone, passes through manifest and null-rule validation, and merges into one unified FOCUS table with provider extensions held in a side table.

AWS: Data Exports for FOCUS

AWS made Data Exports for FOCUS 1.2 generally available on November 19, 2025, according to the AWS announcement. You create the export in the Data Exports page of the Billing and Cost Management console using the table named “FOCUS 1.2 with AWS columns”, and files land in Amazon S3. Per the announcement the console flow is offered through US East (N. Virginia) while the data covers all AWS Regions except GovCloud (US) and China (Beijing and Ningxia). The table’s SQL name is FOCUS_1_2_AWS, and its documentation lists time granularity options of hourly, daily or monthly.

Two behaviours deserve attention. A management account’s export includes all member accounts, while a member account sees only its own data, and only since it joined the organization. If your FinOps team lives in a delegated account rather than the management account, check what you actually receive. Second, the AWS documentation page for the table contradicts itself on the FOCUS version (the title says 1.2, body text says 1.0), so read the manifest rather than trusting prose.

AWS also states the export adds capabilities that CUR lacked, such as invoice reconciliation, capacity reservation status tracking and virtual currency support. If you currently run CUR 2.0, you do not need to switch overnight; the migration pattern is to export both for a few billing cycles and diff the totals.

Azure: the FOCUS cost and usage dataset

Microsoft’s schema reference lists three FOCUS-related schema generations: 1.0-preview(v1) with 94 columns, 1.0 with 96, and 1.2-preview with 105, the last labelled as preview. A follow-up called 1.0r2 changed only date formatting to include seconds, which breaks naive parsers that expected 2024-01-01T00:00Z. Several earlier extension columns graduated into the standard (x_InvoiceId became InvoiceId, x_PricingCurrency became PricingCurrency), so a pipeline built against the early preview needs a rename step.

The Azure-specific columns carry real information. x_AmortizationClass marks commitment purchases and refunds as Principal and covered usage as Amortized Charge. x_BillingProfileId and x_InvoiceSectionId capture the Microsoft Customer Agreement hierarchy, and x_CostAllocationRuleName records which Azure cost allocation rule generated a charge. If your finance team uses Azure’s native allocation rules, keep that column; otherwise you cannot explain why numbers differ between Azure’s portal and your warehouse.

GCP: BigQuery view over standard and detailed export

Google Cloud’s approach differs. You keep exporting Cloud Billing data to BigQuery and apply a Google-provided view that reshapes it into FOCUS. The published page I verified describes this at FOCUS v1.0 GA, with a Looker template, and says the view works across the standard export, the detailed export (which includes resource-level data and price fields to be joined with the price export) and the price export. Third-party reporting in October 2026 still calls GCP’s export a preview of a later version; I could not verify that on a primary page, so check the current docs.

The practical implication: GCP is the provider where you are most likely to be one or two versions behind, and where resource-level FOCUS rows depend on enabling the detailed export. Without detailed export, ResourceId is mostly empty and your allocation quality collapses, regardless of how good your tags are.

The column mapping at a glance

FOCUS column AWS native source Azure native source GCP native source
BillingAccountId Payer account Billing account Billing account ID
SubAccountId Linked account Subscription Project
ServiceName Product / service Meter category Service description
ResourceId ARN or resource ID Resource ID Resource global name (detailed export)
Tags Cost allocation tags Resource tags Labels (user and system)
BilledCost Invoiced line amount Cost in billing currency Cost plus credits as billed
EffectiveCost Amortized with commitments Amortized (x_AmortizationClass) Derived with CUD allocation

The right-hand columns are my mapping summary by concept, not a vendor-published crosswalk; verify field-level names against each provider’s FOCUS table before coding. The point of the table is the hierarchy: FOCUS gives you two scopes, BillingAccountId (who pays) and SubAccountId (who owns), and a normalization pipeline has to decide how to map those onto your own org chart.

Where “native FOCUS” still leaves work

Native exports remove column-name translation. They do not remove four problems. First, version skew: AWS at 1.2, Azure at a 1.2 preview and GCP lower means that columns added in 1.2 exist for one provider and are absent for another. Second, completeness: a provider can still publish a month and revise it later; 1.3 adds metadata flags for exactly this, but none of the exports I could verify implement 1.3. Third, tagging: FOCUS carries tags, it does not make teams apply them. Fourth, anything below the provider bill, such as Kubernetes namespaces, is invisible.

Commitment Amortization: What EffectiveCost Actually Does

Direct answer: Commitment amortization spreads the price of a Reserved Instance, Savings Plan or committed use discount across the hours it covers, so that the team consuming the discounted capacity carries the cost. In FOCUS, the purchase row keeps its BilledCost but gets zero EffectiveCost, and the covered usage rows carry the amortized EffectiveCost.

Consider an illustrative example; the numbers are invented to show mechanics, not any real price. A team buys a one-year all-upfront compute commitment for $8,760, which is exactly $1.00 per hour across 8,760 hours. In the invoice month of purchase, the provider bills $8,760. On the invoice view, that month looks like a spike and the following eleven months look cheaper.

In FOCUS terms, the purchase appears as a row with ChargeCategory Purchase, BilledCost 8,760 and EffectiveCost 0. Each hour of covered usage appears as a Usage row with BilledCost 0 for the covered portion and EffectiveCost $1.00 (or less, if the usage was smaller than the commitment). In a 730-hour month with full utilization, EffectiveCost sums to $730 for covered usage, and the identity holds across the whole term: total Usage EffectiveCost for that commitment equals the total Purchase BilledCost.

Now add waste. Suppose the team’s covered workload only consumes $0.80 of the $1.00 hourly commitment. The remaining $0.20 per hour is unused commitment, which should appear as its own rows carrying a commitment status of unused (the exact representation varies by provider, so verify it in your export). Over 730 hours that is $146 per month of cost that the invoice shows as nothing (it was prepaid) and the effective view shows as a distinct line. This is why FOCUS pairs EffectiveCost with CommitmentDiscountStatus: the unused portion should be assigned to a platform or FinOps cost center, not smeared invisibly across teams, otherwise nobody has an incentive to fix coverage.

Which cost for which question

Question Use Why
Does the dashboard tie to the invoice? BilledCost Matches cash and invoices
What did team A’s workloads cost, fully loaded? EffectiveCost Carries amortized commitments
How good is our negotiated discount? ContractedCost vs ListCost Isolates agreement value
Are we wasting commitments? EffectiveCost where status is Unused Surfaces idle committed capacity
What is true unit price drift? ContractedUnitPrice by SKU over time Excludes commitment amortization noise

A subtle point from the specification: providers choose the amortization method, and someone who wants a different approach can use BilledCost of the original purchase line instead. For recurring commitment fees the specification says providers should distribute the purchase amount rather than include a row at the beginning of each period. Practically, if your finance team requires straight-line amortization over the contract term and the provider uses a usage-weighted method, your EffectiveCost will legitimately differ from the accounting books. Document which method each provider uses and do not re-amortize silently.

Edge cases in commitment accounting

Refunds and exchanges appear as negative purchase rows, and they interact with Azure’s x_AmortizationClass Principal classification. Mid-term cancellations leave partially amortized balances that no usage row will ever consume. Reservations purchased in one subscription but shared across a billing scope credit usage in other subscriptions, so the team that bought the commitment is rarely the team whose EffectiveCost it reduces. Decide your policy before building dashboards: either charge the buyer (simple, often unfair) or let the benefit follow usage (fair, harder to explain).

The tests that catch these problems are cheap. For each CommitmentDiscountId, assert that the sum of Usage EffectiveCost, counting rows whose commitment status is unused, equals the sum of Purchase BilledCost within a rounding tolerance, once the commitment term has ended. For ongoing commitments, assert that the cumulative amortized amount never exceeds the purchase. Failures usually mean a missing refund row or a billing period you have not loaded yet.

A FinOps Pipeline: Object Store to DuckDB to dbt to Dashboards

Direct answer: A robust FinOps pipeline keeps provider exports immutable in object storage, reads them with a columnar engine such as DuckDB or a cloud warehouse, models them with dbt into a union of FOCUS rows plus allocation marts, and gates every refresh with reconciliation tests against the invoice. Normalization is cheap; trust is what you are really building.

FinOps pipeline from provider FOCUS exports through object store, DuckDB, dbt models, allocation marts and dashboards with reconciliation tests

Figure 3: Reference FinOps pipeline. Raw files are never edited; dbt builds staging and core models; reconciliation tests and drift alerts sit beside the marts, not after them.

Why this shape

Billing exports are append-and-revise data. A provider writes a monthly or daily partition, then rewrites it as late charges, credits and corrections arrive. Immutable raw storage with a partition per export run lets you replay any past state, which is what auditors ask for when a number changes. The raw zone should record the export manifest, because it names the FOCUS version and tells you which columns to expect.

DuckDB suits small and medium estates well. It reads Parquet and CSV directly from S3-compatible storage, runs on a single machine, and a mid-size enterprise’s FOCUS data is often gigabytes, not terabytes. When data grows into billions of rows, or when many analysts need concurrent access, move the same dbt project to BigQuery, Snowflake or Databricks; the SQL changes little because FOCUS gave you a stable schema. Be honest about the threshold: with hourly granularity and resource-level rows, a large AWS estate can generate very large exports, so size from your own row counts rather than rules of thumb.

Staging: one adapter per provider

The staging layer does three things only. It selects the FOCUS columns you rely on, casts types, and records provenance (provider, spec version, source file, load timestamp). It does not allocate, filter or fix anything, so that you can always diff staging against the raw export. Here is a runnable DuckDB example using local Parquet files; swap the paths for s3:// URLs after INSTALL httpfs; LOAD httpfs;.

-- 1. Stage each provider into a common shape
CREATE OR REPLACE VIEW stg_aws AS
SELECT 'aws' AS provider, '1.2' AS focus_version,
       BillingAccountId, SubAccountId, BillingPeriodStart,
       ChargePeriodStart, ChargePeriodEnd, ServiceName, RegionId,
       ResourceId, ChargeCategory, ChargeClass, PricingCategory,
       CommitmentDiscountId, CommitmentDiscountStatus,
       CAST(ListCost AS DECIMAL(18,6))       AS ListCost,
       CAST(ContractedCost AS DECIMAL(18,6)) AS ContractedCost,
       CAST(BilledCost AS DECIMAL(18,6))     AS BilledCost,
       CAST(EffectiveCost AS DECIMAL(18,6))  AS EffectiveCost,
       BillingCurrency, Tags
FROM read_parquet('raw/aws/*/*.parquet');

CREATE OR REPLACE VIEW stg_azure AS
SELECT 'azure' AS provider, '1.2-preview' AS focus_version,
       BillingAccountId, SubAccountId, BillingPeriodStart,
       ChargePeriodStart, ChargePeriodEnd, ServiceName, RegionId,
       ResourceId, ChargeCategory, ChargeClass, PricingCategory,
       CommitmentDiscountId, CommitmentDiscountStatus,
       CAST(ListCost AS DECIMAL(18,6)), CAST(ContractedCost AS DECIMAL(18,6)),
       CAST(BilledCost AS DECIMAL(18,6)), CAST(EffectiveCost AS DECIMAL(18,6)),
       BillingCurrency, Tags
FROM read_csv_auto('raw/azure/*/*.csv');

CREATE OR REPLACE VIEW stg_gcp AS
SELECT 'gcp' AS provider, '1.0' AS focus_version,
       BillingAccountId, SubAccountId, BillingPeriodStart,
       ChargePeriodStart, ChargePeriodEnd, ServiceName, RegionId,
       ResourceId, ChargeCategory, ChargeClass, PricingCategory,
       CommitmentDiscountId, CommitmentDiscountStatus,
       CAST(ListCost AS DECIMAL(18,6)), CAST(ContractedCost AS DECIMAL(18,6)),
       CAST(BilledCost AS DECIMAL(18,6)), CAST(EffectiveCost AS DECIMAL(18,6)),
       BillingCurrency, Tags
FROM read_parquet('raw/gcp/*/*.parquet');

-- 2. Core: union plus monthly rollup by provider and currency
CREATE OR REPLACE VIEW core_focus AS
SELECT * FROM stg_aws UNION ALL BY NAME
SELECT * FROM stg_azure UNION ALL BY NAME
SELECT * FROM stg_gcp;

SELECT provider, BillingCurrency,
       date_trunc('month', CAST(BillingPeriodStart AS TIMESTAMP)) AS month,
       SUM(BilledCost)    AS billed,
       SUM(EffectiveCost) AS effective,
       SUM(ListCost) - SUM(ContractedCost) AS negotiated_discount
FROM core_focus
GROUP BY ALL ORDER BY month, provider;

Two details in that snippet carry the weight. Casting costs to fixed decimals avoids floating-point drift that makes reconciliation tests flaky. And grouping by BillingCurrency prevents the classic bug of summing dollars and euros because three exports each used the account currency. FOCUS gives you BillingCurrency on every row; it does not convert it for you, so conversion is a deliberate, dated join to an exchange-rate table.

A Python wrapper adds the two checks that matter most before anything is published: a version gate and a reconciliation gate.

import duckdb, json, sys

con = duckdb.connect("finops.duckdb")
con.execute(open("focus_views.sql").read())

SUPPORTED = {"aws": {"1.2"}, "azure": {"1.2-preview", "1.0"}, "gcp": {"1.0"}}

# Gate 1 - manifest version must be one we have an adapter for
rows = con.execute(
    "SELECT DISTINCT provider, focus_version FROM core_focus").fetchall()
for provider, ver in rows:
    if ver not in SUPPORTED[provider]:
        sys.exit(f"Unsupported {provider} FOCUS version {ver}")

# Gate 2 - billed cost must reconcile to invoice totals (loaded separately)
bad = con.execute("""
    SELECT c.provider, c.month, c.billed, i.invoice_total,
           ABS(c.billed - i.invoice_total) AS delta
    FROM (SELECT provider, date_trunc('month', CAST(BillingPeriodStart AS TIMESTAMP)) AS month,
                 SUM(BilledCost) AS billed FROM core_focus GROUP BY ALL) c
    JOIN invoice_totals i USING (provider, month)
    WHERE ABS(c.billed - i.invoice_total) > 0.005 * i.invoice_total
""").fetchall()
if bad:
    print(json.dumps(bad, default=str, indent=2)); sys.exit("Reconciliation failed")
print("ok")

The 0.5% tolerance is an illustrative choice; set yours from the rounding and tax treatment of your own invoices, and note that FOCUS’s own guidance says period totals of EffectiveCost need not match invoices, which is why the gate uses BilledCost.

dbt on top

dbt earns its place with three things DuckDB views alone do not give you: documented lineage, tests that fail the build, and incremental materialization. The incremental pattern for cost data is a partition replace by billing period, not an append, because revised months must overwrite the old rows. A typical test set is: not-null on the mandatory cost columns; accepted values on ChargeCategory restricted to the list your target spec version defines; a singular test enforcing the commitment identity; and a freshness check against ChargePeriodEnd. If the provider exposes the 1.3 completeness flag, fail dashboards that read a month marked incomplete.

Pair the table with a lightweight dimension layer: an account-to-owner mapping, a service-to-category mapping (compute, storage, network, data, AI), and a region-to-geography mapping. FOCUS standardizes ServiceCategory, which removes most of the service-name juggling, but organization-specific groupings still belong in your dimensions.

Cost of the pipeline itself

Observability and cost pipelines share a trap: the data about spend becomes a spend line. Hourly resource-level FOCUS rows are high-cardinality, and storing every one in a hot warehouse can cost real money. The same discipline we apply to telemetry in tail-based sampling with the OpenTelemetry Collector applies here: keep hourly detail in cheap columnar storage for a short window, aggregate to daily for the long-lived marts, and promote detail back only when an investigation needs it.

Shared-Cost Allocation: From Provider Bill to Team Ledger

Direct answer: Cloud cost allocation turns EffectiveCost rows into per-team or per-product numbers in three passes: direct attribution by tags or account, proportional distribution of shared pools using a declared driver, and an explicit unallocated bucket for what remains. Any allocation that hides the unallocated remainder will eventually be distrusted.

Cloud cost allocation sequence showing raw FOCUS rows flowing through direct tag attribution, shared pool distribution and an unallocated bucket into a team ledger

Figure 4: Allocation as a ledger. Direct costs, shared pools and unallocated spend all stay visible, and totals must tie back to EffectiveCost.

Pass one: direct attribution

Start with what is unambiguous. Rows with a team or product tag, or sitting in an account dedicated to one team, are assigned directly. The Tags column in FOCUS is a JSON object of key and value pairs, so extraction is a single expression in DuckDB (json_extract_string(Tags, '$.team')). Normalize keys before you trust them, since Team, team and owner are all in the wild, and providers may apply case rules differently.

Tag coverage is a metric, not an assumption. Measure the fraction of EffectiveCost with a valid team tag per provider per month, publish it, and make it a release gate for the allocation mart. Expect the first measurement to be uncomfortable, and do not benchmark yourself against anyone else’s number. The coverage number also tells you how much of the final answer is allocation policy rather than observation.

Pass two: shared pools and drivers

Shared costs include networking, platform clusters, observability, support plans, enterprise agreement fees and untagged infrastructure. Each pool gets a declared driver: proportional to direct spend, proportional to request count, evenly split, or proportional to a measured usage metric. A worked illustration, with invented numbers: a shared Kubernetes platform costs $40,000 in a month. Team A’s namespaces used 50% of CPU requests, Team B 30%, Team C 20%. Allocate $20,000, $12,000 and $8,000 respectively. If instead you allocated by direct cloud spend, a team with big storage bills but tiny cluster usage would be overcharged for compute it never used.

FOCUS 1.3 formalizes part of this. Its Cost and Usage section includes allocation columns, Allocated Method ID, Allocated Method Details, Allocated Resource ID, Allocated Resource Name and Allocated Tags, which let a provider show how it divided a shared resource such as a Kubernetes cluster or database instance across workloads, so you see the method as well as the result. I could not confirm from the page alone which of these were new in 1.3, but the Linux Foundation release attributes the allocation capability to that version. Today, no verified export of the three clouds I checked populates 1.3, so for Kubernetes you will still source allocation from in-cluster tooling and join on resource ID.

Pass three: the unallocated bucket

Whatever your drivers cannot assign must remain visible under a clear label such as Unallocated or Platform. Hiding it by spreading it proportionally to everyone feels tidy and destroys the incentive to tag. Visibility creates the pressure that fixes coverage, and you can track the bucket’s share month over month as a program KPI.

The most important invariant is conservation. For any month and currency, the sum of allocated cost across all teams, plus the unallocated bucket, must equal the EffectiveCost total. Encode that as a failing dbt test. Allocation logic changes over time, so version the rule set and store which version produced each month; retroactive changes to a closed month need an explicit restatement process, or finance will find the discrepancy before you do.

Showback versus chargeback

Showback reports allocated cost without moving money; chargeback moves it through internal journals. FOCUS makes either feasible, but chargeback raises the bar on explainability, because a team disputing a charge needs to trace it to concrete rows. Because every dashboard figure derives from FOCUS rows with stable identifiers, your pipeline can answer “which rows made up this number?” in one query, and that is worth more than any particular allocation formula.

Unit Economics: Cost per Thing That Matters

Total spend is a vanity number until you divide it by something the business recognizes. Unit economics turns allocated EffectiveCost into cost per active customer, per thousand API requests, per device connected, or per gigabyte processed. The numerator comes from your FOCUS marts; the denominator comes from product telemetry, and the join key is usually a time bucket plus an allocation key such as product or environment.

Take an illustrative IoT platform. In one month, the ingestion product’s allocated EffectiveCost is $18,000 and it handled 600 million messages from 120,000 connected devices. Cost per million messages is $30 and cost per device is $0.15. Those two numbers behave differently: the first moves with traffic and architecture changes, the second moves with fleet size and chatty-device behaviour. If a firmware update doubles reporting frequency, the per-device number reveals it while the total might hide it inside seasonal growth. The figures are invented for the mechanics.

Choose cost columns deliberately. Unit economics for engineering decisions should use EffectiveCost so that commitment benefits are included, otherwise the month you buy a commitment looks catastrophic. Unit economics for gross-margin reporting may need BilledCost aligned with accounting policy, or a finance-approved amortization. Decide once, label the metric, and never mix them on one chart.

Newer cost drivers

AI inference is now a line item in many bills, and providers expose it as ordinary SKUs. FOCUS gives you ServiceName, SkuId and consumed quantities for model usage, but the interesting engineering variable is cost per request or per outcome, which depends on model choice and routing. Our analysis of LLM routing and model cascades with RouteLLM and FrugalGPT shows how the quality-versus-cost decision is made; FOCUS is the data layer that lets you verify the savings actually appear on the bill instead of in a slide. SaaS vendors are also joining the format, with the Linux Foundation’s 1.3 announcement noting that dataset generators now include SaaS, software, data and platform providers, so the same schema can eventually cover AI vendors outside the big three.

Trade-offs, Gotchas, and What Goes Wrong

FOCUS is a normalization standard, not an authority on truth. The first failure mode is false confidence: two clouds both emit EffectiveCost, but each amortizes by its own method, so cross-cloud comparison of effective rates is approximate. The specification explicitly lets providers choose the amortization method. Use EffectiveCost to compare teams within the same method, and use ContractedCost when comparing price levels across providers.

The second is version skew, discussed earlier. A query written against 1.2 columns breaks on a 1.0 export, and the failure is usually a null column rather than an error, so dashboards go quietly wrong. Mitigate with the version gate shown above and by treating each provider adapter as code with its own tests. When providers upgrade, run old and new exports side by side for a billing cycle and diff.

Third, preview status. Microsoft labels its 1.2 schema as preview, and Google’s FOCUS path has been described as preview in some third-party reporting. Preview schemas can change column semantics, and the 1.0r2 date-format change on Azure shows that even small revisions break parsers. Pin versions and read changelogs.

Fourth, completeness and revision. Late-arriving charges mean the current month is provisional. Without the 1.3 completeness flag in your export, you must implement your own rule, for example treating a billing period as final only after a set number of days past period end, and re-extracting it until then. A dashboard that shows a half-loaded month as a savings win is a credibility injury.

Fifth, currency. Billing currency and pricing currency can differ, and enterprise agreements may bill in local currency while list prices are in dollars. Convert at the stage where finance policy says to, with dated rates, and store the rate used.

Sixth, anti-patterns: re-amortizing commitments yourself and then double counting with EffectiveCost; allocating shared cost before removing Tax and Credit rows; dropping rows with null ResourceId, which are often legitimate account-level charges; and using tags as the only allocation key with no coverage KPI. Finally, remember that FOCUS describes provider bills. It will not tell you which pod caused a cost, and it will not reconcile to your internal general ledger without additional mapping.

Practical Recommendations

Adopt FOCUS as your internal contract even if a provider’s native export is imperfect. Your core model should expect FOCUS columns, and provider adapters should be thin shims that either rename native fields or load the provider’s FOCUS export. That keeps the blast radius of a provider change inside one adapter.

Start with the provider that dominates spend, turn on its native FOCUS export, and run it in parallel with your existing report for two billing cycles. Reconcile BilledCost to invoices first, then EffectiveCost to your commitment ledger. Only then add the second and third providers. Resist building allocation before the reconciliation passes; allocation on untrusted totals multiplies mistakes.

Make the following a checklist for the first quarter:

  • Record the FOCUS version from each export manifest and fail the build on unsupported versions.
  • Land raw exports immutably, partitioned by billing period and export run.
  • Keep costs as fixed-point decimals and group by BillingCurrency everywhere.
  • Test the commitment identity per CommitmentDiscountId and the invoice reconciliation per provider and month.
  • Publish tag coverage per provider and a visible Unallocated bucket.
  • Store provider x_ columns in a side table; promote one to the core model only when a second provider gets an equivalent.
  • Document which cost column each dashboard uses, and never mix BilledCost and EffectiveCost on one axis.
  • Subscribe to specification release notes, including the v1.5 roadmap the site links, and schedule an annual adapter review.

Finally, treat this as a data product with an owner, service levels for freshness and an escalation path when numbers move. The technical work is modest; the organizational work of agreeing on definitions is where programs succeed or stall.

Frequently Asked Questions

What is the FinOps FOCUS specification?

FOCUS, the FinOps Open Cost and Usage Specification, is an open schema for cloud and technology billing data maintained under the FinOps Foundation, part of the Linux Foundation. It defines standard column names, data types, enumerations and rules, so cost data from AWS, Azure, Google Cloud and other providers can be queried together without custom translation. It standardizes the data layout, not the prices, discounts or amortization methods themselves.

Which version of FOCUS is current, and do clouds export it?

The specification site labels version 1.4 as current, following 1.3 announced December 11, 2025. Provider exports lag: AWS announced FOCUS 1.2 general availability on November 19, 2025, Microsoft lists a 1.2 preview schema, and Google documents a BigQuery view at 1.0 GA. Reporting suggests none exports 1.4 yet, but verify against your export manifest, since versions change quickly.

What is the difference between BilledCost and EffectiveCost?

BilledCost is the amount charged on the invoice for a row and is the column to reconcile with invoices. EffectiveCost is the amortized cost after discounts and the applicable share of prepaid commitment purchases. A commitment purchase row has high BilledCost and zero EffectiveCost, while covered usage rows carry the amortized amount. Use BilledCost for finance and cash, EffectiveCost for team-level cost attribution.

Do I still need CUR, Azure exports and the GCP export if I use FOCUS?

Usually yes for a transition period. Native FOCUS exports cover the common columns, but provider-specific detail such as Azure resource group sits in x_ columns and some tools still depend on legacy formats. A sensible path is running both for a few billing cycles, diffing totals, and retiring the legacy pipeline only when stakeholders trust the FOCUS-based numbers and every needed field is available.

How do I handle shared costs with FOCUS?

Do it in layers: attribute direct costs by tags or accounts, distribute shared pools using a declared driver such as CPU requests, and keep an explicit unallocated bucket. FOCUS 1.3 added allocation columns that let providers describe how they split shared resources, such as Kubernetes pods, but real exports rarely populate them yet, so in-cluster tools are still needed. Always test that allocated plus unallocated equals EffectiveCost.

What tools can run the pipeline?

DuckDB reading Parquet from object storage plus dbt is enough for many estates and is cheap to run. Larger organizations use BigQuery, Snowflake or Databricks with the same dbt models. Commercial FinOps platforms increasingly import and export FOCUS, so the schema also lets you move data between tools. Choose by data volume, concurrency and the team’s operational capacity rather than by feature lists.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *