Skip to main content
When you run dbt on BigQuery, it is hard to know which models drive the most cost. BigQuery billing exports show project-level and dataset-level spend, with no model-level granularity. This guide uses BigQuery labels and dbt macros to break down processing and storage costs by model, team, package, or any custom dimension.

Prerequisites

Output

  • Cost visibility broken down by dbt model, team, package, or any custom label.
  • Ability to identify which models drive auto-scaling or high processing costs.
  • Foundation for cost allocation and optimization.

Steps

1

Label your dbt operations in BigQuery

Configure automated labels in your dbt_project.yml so every query is tagged with dbt native metadata:
Then create a macro bq_labels to populate the labels automatically. Here is all the information you can use:
This macro will label every query run during the dbt run.
2

Label your storage resources

In BigQuery, storage costs are visible at the dataset level using resource_id / resource_name, but they are not visible at the billing level per table. To add labels at the table level, apply them in your dbt model config:
You can also set labels at the dbt_project.yml level. Note that project-level labels will override model-level labels if both are set.
3

Visualize your labeled job costs without Costory

Use BigQuery’s INFORMATION_SCHEMA to analyze costs by label:
To query storage-level labels on tables:
4

Visualize your labeled job costs with Costory

  1. Ingest the labels into Costory using the feature engineering UI: Costory feature engineering screen showing how to ingest BigQuery labels from dbt models
  2. If part of your BigQuery labels should be merged within an existing label, use the merge feature to combine two labels into one (e.g., k8s_label_app and app).
  3. Create a virtual dimension to allocate the cost, or explore costs directly in Explorer: Costory Explorer showing BigQuery costs broken down by dbt model, team, and package labels
To investigate a spike without opening Explorer, ask the Costory MCP: “split last week’s BigQuery cost by the node_name label and show the top 10 dbt models.”

Best practices

Labels

  • Ingest node_name for fine-grained cost investigation to understand which exact model is driving cost.
  • Use package_name or schema to map each schema to the correct team or owner. You can formalize this mapping with virtual dimensions.
  • Use target_name to split daily jobs, full refreshes, and backfills.

Quotas

Alerts

  • Set an alert close to the daily average on ingestion costs. Daily ingestion jobs should be very stable.
  • For full refresh jobs, evaluate alerts over a larger period to avoid false positives from spikes.

Storage costs

  • Evaluate storage costs frequently. After 90 days the storage price decreases, but if you never read the data you should rely on an expiration policy instead.
  • Since 2023, you can choose per dataset the storage billing model:
    • Logical storage: you pay based on the logical size (before compression).
    • Physical storage: you pay based on the physical size (after compression).
You can compare both costs using the following SQL query. Run it per region and change the region-us suffix:
Tabular data with frequent values usually compresses well, so the physical cost is typically lower than the logical cost. If you find a dataset that would be significantly cheaper under the physical model:
Changes take about 24 hours to affect billing. Once you switch, you must wait 14 days before you can switch back.

Next steps

FAQ

dbt supports job-level labels via query-comment in dbt_project.yml and table-level labels via the labels property in model config(). Job labels appear in INFORMATION_SCHEMA.JOBS_BY_PROJECT, while table labels appear in INFORMATION_SCHEMA.TABLE_OPTIONS.
Physical storage costs are the costs of the data stored in BigQuery. Logical storage costs are the costs of the data stored in BigQuery after compression. You can compare both costs using the SQL query above. Usually tabular data with frequent values compresses well, so the physical cost is typically lower than the logical cost.
Use GCP’s custom quotas feature with QueryUsagePerUserPerDay to limit bytes billed per user or service account per day.
Yes. Labels set in dbt_project.yml at the project level will override labels set in individual model config() blocks if both define the same key.
When using slots, you will need to reattribute the slots costs to each query. You can do this using virtual dimensions. A native connector is under development to do it natively.
Last modified on June 30, 2026