skip to content

Finance wants hourly cost per team tag, broken down by usage type, for the last twelve months — more detail than the Billing console will show. Explain what the AWS Cost and Usage Report gives you that the console does not, and how you would query it.

level: seniorimportance: should knowfreq 40%

answer

  1. the raw rows behind every console view
  2. one row per line item per hour
  3. activated tags become real columns
  4. partition or pay for the scan
  5. the open month gets rewritten

basics

~20 s

The Cost and Usage Report delivers the raw billing line items to an S3 bucket you own, at hourly granularity with activated tags and optionally resource IDs as columns. You query it with Athena over the S3 data, which gives detail and joins the console cannot express.

solid answer

~60 s

The console shows aggregated, pre-shaped views. The **Cost and Usage Report (CUR)** is the underlying data: AWS writes files to an S3 bucket you own, one row per billing line item per period, with columns grouped by prefix — `bill/`, `lineItem/`, `product/`, `pricing/`, and one `resourceTags/` column for each *activated* cost allocation tag. Choose hourly granularity and include resource IDs and you can answer questions the console cannot, such as cost per tag per usage type per hour, or joins against your own inventory data. You point Athena at the bucket — AWS ships a Glue crawler and a CloudFormation setup for exactly this — and write SQL. The operational catches are real: hourly reports with resource IDs get very large, so you partition by billing period and prefer Parquet; AWS re-delivers the current month's files repeatedly and finalises after month end, so downstream ETL must handle replacement rather than append; only the management account gets the organization-wide report; and tags only appear as columns if they were activated when the usage was metered. Newer AWS Data Exports lets you select columns and export CUR 2.0 or FOCUS-format data directly in Parquet.

code

sql · 8 lines
sql
SELECT resource_tags_user_cost_center AS cost_center,
       line_item_usage_type,
       sum(line_item_unblended_cost) AS cost
FROM   cur_db.my_report
WHERE  bill_billing_period_start_date = TIMESTAMP '2025-07-01 00:00:00'
GROUP  BY 1, 2
ORDER  BY cost DESC
LIMIT  50;

go deeper

for a junior

Know that AWS can deliver detailed billing data as files to an S3 bucket you own, and that activated tags appear there as columns. Naming the Cost and Usage Report and Athena as the query engine is enough.

for a middle

Explain the row model — one line item per usage period, columns namespaced by prefix, one column per activated tag — and why you would choose it over the console: hourly detail, resource IDs, and joins to your own data.

for a senior

Show operational scars: partition and use Parquet or pay for the scan, handle the open month being rewritten, know that line item types make a naive sum wrong, and recognise when the console is the cheaper answer.

for a principal

Decide whether the organization runs a cost data platform at all. Weigh the pipeline's own cost and maintenance against what the console already answers, pick one canonical export format such as FOCUS if multiple clouds are in play, and set who owns the numbers finance reports.

## What the CUR actually is Every other cost view in AWS is a rendering of the same underlying billing records. The **Cost and Usage Report** hands you those records. You configure it once in the Billing and Cost Management console: a report name, an S3 bucket you own (with a bucket policy allowing the billing service to write), a time granularity of hourly, daily or monthly, and options for including resource IDs and split cost allocation data. AWS then delivers compressed files — CSV or Parquet — plus a manifest per billing period, and refreshes them several times a day while the month is open. ## The row model One row is one **line item**: a charge for one usage type, in one operation, in one hour, for one account. That means a single EC2 instance running for a day produces many rows, and one instance-hour can produce several rows of different `lineItem/LineItemType` — usage, the discount line if a commitment covered it, taxes, credits, fees. Anyone summing a cost column without understanding line item types will produce a number that does not match the invoice, which is the classic first mistake. Columns are namespaced by prefix: - `bill/` — billing period, payer account, invoice id - `lineItem/` — account, usage start and end, usage type, operation, usage amount, cost, and the line item type - `product/` — attributes of what was used: region, instance type, and so on - `pricing/` — the rate and its unit - `resourceTags/` — **one column per activated cost allocation tag**, which is the whole reason this leaf cares about the CUR That last point is the connection back to tagging: an activated tag becomes a first-class column in the raw data, so allocation stops being a console filter and becomes SQL you control. ## Querying it with Athena Athena reads directly from S3, so there is no load step. AWS provides a crawler and a CloudFormation-based integration that creates the Glue table and keeps partitions current; in the Athena table, CUR's slashed column names are flattened to underscores. ```sql SELECT resource_tags_user_cost_center AS cost_center, line_item_usage_type, sum(line_item_unblended_cost) AS cost FROM cur_db.my_report WHERE bill_billing_period_start_date = TIMESTAMP '2025-07-01 00:00:00' GROUP BY 1, 2 ORDER BY cost DESC; ``` The things that make this survive contact with production: **Partition, always.** Filter on the billing period partition in every query. Athena charges by bytes scanned, and an unpartitioned twelve-month hourly report with resource IDs is exactly the kind of table where a careless `SELECT *` becomes a line item of its own. **Prefer Parquet.** Columnar storage plus the fact that most cost queries touch a handful of the report's very many columns is a large reduction in scanned bytes. **Handle re-delivery.** During an open month AWS overwrites the period's data as the estimate is refined, and the report is finalised after the month closes. A pipeline that appends rather than replaces will double-count. Report versioning options control whether new deliveries overwrite the existing files or create a new version, and your ingestion must match whichever you chose. **Only the payer gets the whole picture.** The management account's report covers the organization; a member account can enable its own report covering only itself. **Tags are only there if they were activated.** A tag activated last week does not retroactively populate a column for last quarter, so the twelve-month request in the question may simply be unanswerable for the earlier months — say so rather than producing a report full of nulls without comment. ## Newer delivery paths AWS Data Exports is the current mechanism for creating these exports; alongside the legacy CUR it offers CUR 2.0 with a flattened schema and column selection, and a FOCUS-format export for organizations standardising cost data across more than one cloud. Same S3-and-query pattern, less schema to carry. ## When *not* to reach for it If the question is "what did we spend last month by service", the console answers it in seconds and building a data pipeline is waste. The CUR earns its keep when you need resource-level or hourly detail, joins to data AWS does not have (your CMDB, your team roster, per-tenant usage), or an allocation model whose logic is more complicated than the console's grouping options can express.

  • Why does summing a cost column across every CUR row not equal the invoice?
    Because rows are not all charges. `lineItem/LineItemType` distinguishes usage from taxes, fees, credits, refunds and the discount lines produced when a commitment covers usage — some of which are negative or duplicate a covered usage row. Reconciling to an invoice means grouping by line item type and deciding deliberately which types belong in the number you are reporting.
  • Your Athena bill jumped after adding the CUR table. What went wrong?
    Almost certainly unpartitioned or non-columnar scanning. Athena charges by bytes scanned, and an hourly report with resource IDs over twelve months is enormous. Filter on the billing period partition in every query, store the report as Parquet, keep partitions registered by the crawler or projection, and never run exploratory `SELECT *` against the raw table.
  • Finance wants a year of data grouped by a tag activated last month. What do you tell them?
    That the earlier months cannot carry that tag: the CUR records the tag values present when usage was metered, so the column is empty before activation. Options are the Billing console's tag backfill for a limited historical window, or a Cost Category with a backdated effective start that maps accounts and services to teams without needing per-resource tags.
  • How would you get per-pod cost for teams sharing one EKS cluster?
    Enable split cost allocation data on the report, which breaks a cluster's or task's cost down to individual pods and tasks based on their resource requests, and emits the Kubernetes labels alongside. That turns a single shared cluster line item into rows a chargeback query can group by team, without needing the shared instances themselves tagged per team.

saying these in an interview costs you the question

  • Summing every CUR row and expecting the invoice total
  • Querying the raw table without a billing-period filter
  • Expecting tags to appear for months before activation
  • Assuming a member account can produce the org-wide report
  • Appending each delivery instead of replacing the month

context